node-mac-recorder 2.24.4 → 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,
@@ -899,79 +903,58 @@ class MacRecorder extends EventEmitter {
899
903
  }
900
904
  };
901
905
 
902
- let videoStartTimestamp = 0;
903
- const waitStart = Date.now();
904
-
905
- if (usesScreenCaptureKit) {
906
- while (Date.now() - waitStart < VIDEO_START_TIMEOUT_MS) {
907
- videoStartTimestamp = readVideoStart();
908
- if (videoStartTimestamp > 0) {
909
- console.log(
910
- `✅ SYNC: Video ilk karesi ${Date.now() - waitStart}ms'de yakalandi`
911
- );
912
- break;
913
- }
914
- await new Promise(r => setTimeout(r, VIDEO_START_POLL_MS));
915
- }
916
-
917
- if (!videoStartTimestamp) {
918
- console.warn(
919
- `⚠️ SYNC: Video baslangici ${VIDEO_START_TIMEOUT_MS}ms icinde okunamadi — heuristik hizalamaya dusuluyor`
920
- );
921
- }
922
- } else {
923
- // AVFoundation yolunda video-start damgasi yok; eski
924
- // hazir-olma beklemesi korunuyor.
925
- while (Date.now() - waitStart < 600) {
926
- try {
927
- if (
928
- nativeBinding &&
929
- nativeBinding.getRecordingStatus &&
930
- nativeBinding.getRecordingStatus()
931
- ) {
932
- break;
933
- }
934
- } catch (_) {}
935
- await new Promise(r => setTimeout(r, 30));
936
- }
937
- }
938
-
906
+ // ONEMLI: Bu bekleme BLOKLAMAZ.
907
+ // Ilk kare, yakalandigi andan ~200ms+ sonra islenip bize ulasiyor
908
+ // (clamshell'de saniyeler). startRecording bunu beklerse kayit
909
+ // coktan basladigi halde UI "Starting..." gostergesinde takili
910
+ // kaliyor. Bu yuzden cursor/klavye HEMEN baslatiliyor (veri kaybi
911
+ // olmasin), video baslangici ARKA PLANDA yakalanip saklaniyor;
912
+ // stop sirasinda cursor JSON'una gercek offset yaziliyor ve
913
+ // editor bunu birebir telafi ediyor.
939
914
  this.sessionTimestamp = sessionTimestamp;
940
- this.videoStartTimestamp = videoStartTimestamp;
941
- // Onceki kayittan kalan referans sizmasin
942
- this.timelineStartTimestamp = 0;
915
+ this.videoStartTimestamp = 0;
943
916
 
944
917
  const syncTimestamp = Date.now();
945
918
  this.syncTimestamp = syncTimestamp;
946
919
  this.recordingStartTime = syncTimestamp;
920
+ this.timelineStartTimestamp = syncTimestamp;
947
921
 
948
- if (videoStartTimestamp > 0) {
949
- console.log(
950
- `🎯 SYNC: Video first-frame timestamp: ${videoStartTimestamp} (JS bunu ${(syncTimestamp - videoStartTimestamp).toFixed(0)}ms sonra fark etti)`
951
- );
922
+ if (usesScreenCaptureKit) {
923
+ const watchStart = Date.now();
924
+ this._videoStartWatcherActive = true;
925
+ this._videoStartWatcher = (async () => {
926
+ while (
927
+ this._videoStartWatcherActive &&
928
+ Date.now() - watchStart < VIDEO_START_TIMEOUT_MS
929
+ ) {
930
+ const value = readVideoStart();
931
+ if (value > 0) {
932
+ this.videoStartTimestamp = value;
933
+ console.log(
934
+ `✅ SYNC: Video ilk karesi ${Date.now() - watchStart}ms'de yakalandi (cursor'a gore ${(syncTimestamp - value).toFixed(0)}ms once)`
935
+ );
936
+ return value;
937
+ }
938
+ await new Promise(r => setTimeout(r, VIDEO_START_POLL_MS));
939
+ }
940
+ if (this._videoStartWatcherActive) {
941
+ console.warn(
942
+ `⚠️ SYNC: Video baslangici ${VIDEO_START_TIMEOUT_MS}ms icinde okunamadi — heuristik hizalamaya dusuluyor`
943
+ );
944
+ }
945
+ return 0;
946
+ })();
947
+ // Kayit akisini etkilemesin
948
+ this._videoStartWatcher.catch(() => {});
952
949
  }
953
950
 
954
- // ZAMAN REFERANSI: cursor/klavye timeline'i VIDEONUN ilk karesine
955
- // hizalanir, JS'in "kayit hazir" dedigi ana degil.
956
- //
957
- // NEDEN: syncTimestamp, yukaridaki hazir-olma dongusunden sonraki an.
958
- // Bu an ile videonun gercek t=0'i arasindaki fark, baslatma hizina
959
- // gore DEGISIYOR (ornegin ScreenCaptureKit isitilmissa video cok daha
960
- // erken basliyor). Cursor bu degisken ana baglanirsa her kayitta
961
- // farkli bir kayma olusuyor ve editordeki telafi 1sn limitine
962
- // takilabiliyor. Videonun kendi baslangicini referans alinca fark
963
- // yapisal olarak sifir olur; hizlanma senkronu bozmaz.
964
- const timelineStartTimestamp =
965
- this.videoStartTimestamp > 0
966
- ? this.videoStartTimestamp
967
- : syncTimestamp;
968
- this.timelineStartTimestamp = timelineStartTimestamp;
969
-
970
- if (timelineStartTimestamp !== syncTimestamp) {
971
- console.log(
972
- `🎯 SYNC: Timeline referansi video ilk karesine cekildi (${(syncTimestamp - timelineStartTimestamp).toFixed(0)}ms geri)`
973
- );
974
- }
951
+ // ZAMAN REFERANSI: cursor/klavye timeline'i SIMDI baslar
952
+ // (syncTimestamp). Videonun gercek t=0'i genelde bundan biraz
953
+ // oncedir; aradaki fark stop'ta cursor JSON'una yazilir ve
954
+ // editor birebir telafi eder. Bekleyip "sifir offset" elde
955
+ // etmek yerine bu yol tercih ediliyor cunku bekleme, kayit
956
+ // coktan basladigi halde UI'i "Starting..." halinde tutuyordu.
957
+ const timelineStartTimestamp = syncTimestamp;
975
958
 
976
959
  const standardCursorOptions = {
977
960
  videoRelative: true,
@@ -1085,11 +1068,43 @@ class MacRecorder extends EventEmitter {
1085
1068
  }, 1000);
1086
1069
 
1087
1070
  // Native kayıt gerçekten başladığını kontrol etmek için polling başlat
1071
+ //
1072
+ // KRITIK: getRecordingStatus() TEK BASINA KULLANILAMAZ.
1073
+ // ScreenCaptureKit yolunda `isFullyInitialized` = 10 KARE sarti
1074
+ // arıyor. Ama video writer ILK KAREDE basliyor — yani kayit
1075
+ // coktan basladigi halde bu bayrak false kalabiliyor.
1076
+ // ScreenCaptureKit statik ekranda yeni kare URETMEDIGI icin
1077
+ // (sadece degisiklik oldugunda kare gonderir) kullanici kayda
1078
+ // basip bekledigi anda 10 kare hic dolmuyor ve UI saniyelerce
1079
+ // "Starting recorder..." gosteriyordu; bu sure de videoya giriyordu.
1080
+ //
1081
+ // Dogru sinyal ilk karedir: videoStartTimestamp > 0 (arka plandaki
1082
+ // _videoStartWatcher set eder). Boylece UI, videonun gercek
1083
+ // baslangici ile ayni ana kapanir.
1088
1084
  let recordingStartedEmitted = false;
1089
1085
  let checkRecordingStatus = null;
1086
+ const isNativeRecordingLive = () => {
1087
+ if (this.options.preferScreenCaptureKit === true) {
1088
+ // SCK yolunda TEK dogru sinyal ilk karedir.
1089
+ // getRecordingStatus() burada iki yonlu yaniltiyor:
1090
+ // - SCK stream'i henuz baslamadiysa g_isRecording
1091
+ // fallback'ine dusup ERKEN true doner (video yokken
1092
+ // "kayit basladi" denir)
1093
+ // - SCK basladiysa isFullyInitialized = 10 kare bekler,
1094
+ // statik ekranda bu hic dolmaz ve GEC true doner
1095
+ // Hangisinin kazandigi yarisa bagli; ilk kare ise kesin.
1096
+ return Number(this.videoStartTimestamp) > 0;
1097
+ }
1098
+ // AVFoundation yolu: video-start damgasi yok, eski sinyal
1099
+ try {
1100
+ return nativeBinding.getRecordingStatus();
1101
+ } catch (_) {
1102
+ return false;
1103
+ }
1104
+ };
1090
1105
  const pollRecordingStatus = () => {
1091
1106
  try {
1092
- const nativeStatus = nativeBinding.getRecordingStatus();
1107
+ const nativeStatus = isNativeRecordingLive();
1093
1108
  if (nativeStatus && !recordingStartedEmitted) {
1094
1109
  recordingStartedEmitted = true;
1095
1110
  clearInterval(checkRecordingStatus);
@@ -1229,6 +1244,11 @@ class MacRecorder extends EventEmitter {
1229
1244
  try {
1230
1245
  console.log('🛑 SYNC: Stopping all recording components simultaneously');
1231
1246
 
1247
+ // Video baslangicini bekleyen arka plan izleyicisini sonlandir.
1248
+ // Deger geldiyse zaten this.videoStartTimestamp'e yazildi ve
1249
+ // cursor metadata'sinda kullanilacak.
1250
+ this._videoStartWatcherActive = false;
1251
+
1232
1252
  // SYNC FIX: Stop ALL components at the same time for perfect sync
1233
1253
  // 1. Stop cursor tracking FIRST (it's instant)
1234
1254
  if (this.cursorCaptureInterval) {
@@ -1396,6 +1416,19 @@ class MacRecorder extends EventEmitter {
1396
1416
  };
1397
1417
  }
1398
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
+
1399
1432
  /**
1400
1433
  * macOS'ta kayıt izinlerini kontrol eder
1401
1434
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.24.4",
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
@@ -627,14 +628,39 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
627
628
  // Use ScreenCaptureKit with window exclusion and timeout protection
628
629
  NSError *sckError = nil;
629
630
 
630
- // A/V SYNC: Start camera non-blocking BEFORE ScreenCaptureKit
631
- // Camera warmup overlaps with async SCK initialization
631
+ // A/V SYNC: Kamera SCK'dan ONCE baslatilir VE onayi burada beklenir.
632
+ //
633
+ // ESKIDEN: kamera non-blocking baslatilip SCK hemen schedule
634
+ // ediliyor, kamera onayi SCK'dan SONRA bekleniyordu. Sorun:
635
+ // SCK schedule edilir edilmez video ilk karesini yakalayip
636
+ // KAYDA BASLIYOR, ama JS tarafi hala kamera onayini bekledigi
637
+ // icin (startRecording senkron bir N-API cagrisi, event loop
638
+ // bloke) "kayit basladi" sinyali ~1sn gec gidiyordu.
639
+ // Olculdu: kamerali startRecording 1319ms / kamerasiz 508ms.
640
+ // Bu fark dogrudan videonun basina fazlalik olarak yaziliyor
641
+ // ve UI o sure boyunca "Starting recorder..." gosteriyordu.
642
+ //
643
+ // SIMDI: kamera hazir olduktan SONRA video baslar. Basta
644
+ // fazlalik olusmaz ve kamera ile ekran ayni ana hizalanir.
632
645
  if (captureCamera) {
633
- MRLog(@"🎥 Starting camera recording non-blocking (parallel with SCK init)");
646
+ MRLog(@"🎥 Starting camera recording (before SCK, blocking until ready)");
634
647
  if (!startCameraIfRequested(true, &cameraOutputPath, cameraDeviceId, outputPath, sessionTimestamp)) {
635
648
  MRLog(@"❌ Camera failed to start - aborting recording");
636
649
  return Napi::Boolean::New(env, false);
637
650
  }
651
+
652
+ if (!waitForCameraRecordingStart(8.0)) {
653
+ double cameraStartTs = currentCameraRecordingStartTime();
654
+ if (cameraStartTs > 0 || isCameraRecording()) {
655
+ MRLog(@"⚠️ Camera did not confirm start within 8.0s but appears running; continuing");
656
+ } else {
657
+ MRLog(@"❌ Camera did not signal recording start within 8.0s");
658
+ stopCameraRecording();
659
+ return Napi::Boolean::New(env, false);
660
+ }
661
+ } else {
662
+ MRLog(@"✅ Camera recording confirmed (before SCK start)");
663
+ }
638
664
  }
639
665
 
640
666
  // Start SCK immediately - don't wait for camera confirmation
@@ -647,21 +673,10 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
647
673
  MRLog(@"🎬 RECORDING METHOD: ScreenCaptureKit");
648
674
  MRLog(@"✅ SYNC: ScreenCaptureKit recording started successfully");
649
675
 
650
- // A/V SYNC: Wait for camera AFTER SCK started
651
- if (captureCamera) {
652
- if (!waitForCameraRecordingStart(8.0)) {
653
- double cameraStartTs = currentCameraRecordingStartTime();
654
- if (cameraStartTs > 0 || isCameraRecording()) {
655
- MRLog(@"⚠️ Camera did not confirm start within 8.0s but appears running; continuing");
656
- } else {
657
- MRLog(@"❌ Camera did not signal recording start within 8.0s");
658
- stopCameraRecording();
659
- [ScreenCaptureKitRecorder stopRecording];
660
- return Napi::Boolean::New(env, false);
661
- }
662
- }
663
- MRLog(@"✅ Camera recording confirmed (started in parallel with SCK)");
664
- }
676
+ // NOT: Kamera onayi artik SCK'dan ONCE bekleniyor
677
+ // (yukariya alindi) — burada blocking bir is kalmadi,
678
+ // boylece SCK schedule edildikten sonra JS'e donus
679
+ // gecikmiyor ve videonun basinda fazlalik olusmuyor.
665
680
 
666
681
  g_isRecording = true;
667
682
  MRMarkRecordingStartTimestamp();
@@ -1386,6 +1401,29 @@ Napi::Value GetRecordingStatus(const Napi::CallbackInfo& info) {
1386
1401
  return Napi::Boolean::New(env, g_isRecording);
1387
1402
  }
1388
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
+
1389
1427
  // NAPI Function: Get Window Thumbnail
1390
1428
  Napi::Value GetWindowThumbnail(const Napi::CallbackInfo& info) {
1391
1429
  Napi::Env env = info.Env();
@@ -1724,6 +1762,7 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
1724
1762
  exports.Set(Napi::String::New(env, "getDisplays"), Napi::Function::New(env, GetDisplays));
1725
1763
  exports.Set(Napi::String::New(env, "getWindows"), Napi::Function::New(env, GetWindows));
1726
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));
1727
1766
  exports.Set(Napi::String::New(env, "prewarmScreenCapture"), Napi::Function::New(env, PrewarmScreenCapture));
1728
1767
  exports.Set(Napi::String::New(env, "getVideoStartTimestamp"), Napi::Function::New(env, GetVideoStartTimestamp));
1729
1768
  exports.Set(Napi::String::New(env, "checkPermissions"), Napi::Function::New(env, CheckPermissions));
@@ -165,7 +165,56 @@ 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;
179
+
180
+ // ---- Prewarm edilmis SCShareableContent onbellegi ----
181
+ // Kayit baslatilirken envanter cagrisini tamamen atlayabilmek icin saklanir.
182
+ // ARC KAPALI: retain/release elle yonetiliyor.
183
+ static id g_cachedShareableContent = nil;
184
+ static CFAbsoluteTime g_cachedShareableContentTime = 0;
185
+ // Bayat envanter yanlis pencere/ekran secimine yol acabilir; kisa tutuluyor.
186
+ static const CFAbsoluteTime kShareableContentCacheTTLSeconds = 30.0;
187
+
188
+ static void SCKStoreShareableContent(id content) {
189
+ if (!content) return;
190
+ @synchronized([ScreenCaptureKitRecorder class]) {
191
+ if (g_cachedShareableContent != content) {
192
+ [g_cachedShareableContent release];
193
+ g_cachedShareableContent = [content retain];
194
+ }
195
+ g_cachedShareableContentTime = CFAbsoluteTimeGetCurrent();
196
+ }
197
+ }
198
+
199
+ // Taze onbellek varsa doner ve onbellegi bosaltir (her kayit taze envanterle
200
+ // baslasin diye tek kullanimlik). Yoksa nil.
201
+ static id SCKTakeCachedShareableContent(void) {
202
+ @synchronized([ScreenCaptureKitRecorder class]) {
203
+ if (!g_cachedShareableContent) {
204
+ return nil;
205
+ }
206
+
207
+ CFAbsoluteTime age = CFAbsoluteTimeGetCurrent() - g_cachedShareableContentTime;
208
+ id content = [[g_cachedShareableContent retain] autorelease];
209
+ [g_cachedShareableContent release];
210
+ g_cachedShareableContent = nil;
211
+
212
+ if (age > kShareableContentCacheTTLSeconds) {
213
+ return nil;
214
+ }
215
+ return content;
216
+ }
217
+ }
169
218
 
170
219
  // Quality helpers
171
220
  static NSString *SCKNormalizeQualityPreset(id preset) {
@@ -193,42 +242,88 @@ static CGFloat SCKQualityScaleForPreset(NSString *preset) {
193
242
  return 1.0; // High = full resolution
194
243
  }
195
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.
196
291
  static void SCKQualityBitrateForDimensions(NSString *preset,
197
292
  NSInteger width,
198
293
  NSInteger height,
294
+ NSInteger fps,
199
295
  NSInteger *bitrateOut,
200
- NSInteger *multiplierOut,
296
+ double *bppOut,
201
297
  NSInteger *minOut,
202
298
  NSInteger *maxOut) {
203
299
  NSString *normalized = SCKNormalizeQualityPreset(preset);
204
300
 
205
- NSInteger multiplier = 30;
206
- NSInteger minBitrate = 30 * 1000 * 1000;
207
- 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;
208
308
 
209
309
  if ([normalized isEqualToString:@"low"]) {
210
- multiplier = 10;
310
+ bpp = 0.09;
211
311
  minBitrate = 10 * 1000 * 1000;
212
312
  maxBitrate = 45 * 1000 * 1000;
213
313
  } else if ([normalized isEqualToString:@"medium"]) {
214
- multiplier = 18;
215
- minBitrate = 18 * 1000 * 1000;
216
- maxBitrate = 80 * 1000 * 1000;
217
- } else { // high/default - çözünürlüğe DUYARLI yüksek kalite
218
- // Eski hali sabit 50 Mbps idi: Retina/4K kayıtta çok düşük (hatta medium
219
- // yüksek çözünürlükte high'ı geçebiliyordu). Artık çözünürlükle ölçeklenir.
220
- multiplier = 32; // ~0.53 bpp @60fps
221
- minBitrate = kSCKHighQualityVideoBitrate; // 100 Mbps taban
222
- maxBitrate = 200 * 1000 * 1000; // 200 Mbps tavan (realtime güvenli)
223
- }
224
-
225
- 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;
226
321
  NSInteger bitrate = (NSInteger)base;
227
322
  if (bitrate < minBitrate) bitrate = minBitrate;
228
323
  if (bitrate > maxBitrate) bitrate = maxBitrate;
229
324
 
230
325
  if (bitrateOut) *bitrateOut = bitrate;
231
- if (multiplierOut) *multiplierOut = multiplier;
326
+ if (bppOut) *bppOut = bpp;
232
327
  if (minOut) *minOut = minBitrate;
233
328
  if (maxOut) *maxOut = maxBitrate;
234
329
  }
@@ -370,9 +465,31 @@ static void CleanupWriters(void) {
370
465
  g_videoWriterStarted = NO;
371
466
  g_videoStartTime = kCMTimeInvalid;
372
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
+
373
488
  // Reset frame counting
374
489
  g_frameCount = 0;
375
490
  g_firstFrameTime = 0;
491
+ g_videoFramesAppended = 0;
492
+ g_videoFramesDroppedNotReady = 0;
376
493
  }
377
494
 
378
495
  if (g_audioWriter) {
@@ -409,6 +526,23 @@ extern "C" BOOL MRMixAudioToSingleTrackWithGains(NSString *primaryAudioPath,
409
526
  float systemGain);
410
527
  extern "C" BOOL MRMuxAudioIntoVideo(NSString *videoPath, NSString *audioPath);
411
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
+
412
546
  extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
413
547
  if (!g_audioOutputPath) {
414
548
  return nil;
@@ -506,9 +640,21 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
506
640
  }
507
641
 
508
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
+ }
509
655
  return;
510
656
  }
511
-
657
+
512
658
  CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
513
659
  if (!pixelBuffer) {
514
660
  return;
@@ -557,6 +703,8 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
557
703
  BOOL appended = [adaptor appendPixelBuffer:pixelBuffer withPresentationTime:relativePresentation];
558
704
  if (!appended) {
559
705
  NSLog(@"⚠️ Failed appending pixel buffer: %@", g_videoWriter.error);
706
+ } else {
707
+ g_videoFramesAppended++;
560
708
  }
561
709
 
562
710
  // Frame rate debugging
@@ -780,30 +928,39 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
780
928
  }
781
929
 
782
930
  NSString *normalizedQuality = SCKNormalizeQualityPreset(g_qualityPreset);
931
+ NSInteger encoderFPS = MAX(1, g_targetFPS);
783
932
  NSInteger bitrate = 0;
933
+ double bpp = 0.0;
784
934
  NSInteger minBitrate = 0;
785
935
  NSInteger maxBitrate = 0;
786
- SCKQualityBitrateForDimensions(normalizedQuality, width, height, &bitrate, NULL, &minBitrate, &maxBitrate);
936
+ SCKQualityBitrateForDimensions(normalizedQuality, width, height, encoderFPS,
937
+ &bitrate, &bpp, &minBitrate, &maxBitrate);
787
938
 
788
939
  NSNumber *qualityHint = [normalizedQuality isEqualToString:@"high"] ? @1.0 : ([normalizedQuality isEqualToString:@"medium"] ? @0.9 : @0.85);
789
940
 
790
- 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)",
791
942
  normalizedQuality,
792
943
  (long)width,
793
944
  (long)height,
945
+ (long)encoderFPS,
794
946
  bitrate / (1000.0 * 1000.0),
947
+ bpp,
795
948
  (long)(minBitrate / (1000 * 1000)),
796
949
  (long)(maxBitrate / (1000 * 1000)));
797
950
 
798
951
  NSDictionary *compressionProps = @{
799
952
  AVVideoAverageBitRateKey: @(bitrate),
800
- AVVideoMaxKeyFrameIntervalKey: @(MAX(1, g_targetFPS)),
801
- AVVideoAllowFrameReorderingKey: @YES,
802
- 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),
803
960
  AVVideoQualityKey: qualityHint,
804
961
  AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel,
805
962
  AVVideoH264EntropyModeKey: AVVideoH264EntropyModeCABAC,
806
- AVVideoAverageNonDroppableFrameRateKey: @(MAX(1, g_targetFPS)),
963
+ AVVideoAverageNonDroppableFrameRateKey: @(encoderFPS),
807
964
  AVVideoMaxKeyFrameIntervalDurationKey: @(1.0)
808
965
  };
809
966
 
@@ -1041,16 +1198,24 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
1041
1198
 
1042
1199
  + (void)prewarmShareableContent {
1043
1200
  if (@available(macOS 15.0, *)) {
1044
- // Kayit sirasindaki ilk SCShareableContent cagrisi yavas olabiliyor
1045
- // (TCC kontrolu + pencere/ekran envanteri). Kayit baslamadan once bir kez
1046
- // cagirip macOS'un ic cache'ini isitiyoruz. Sonuc kullanilmiyor.
1201
+ // Kayit sirasindaki SCShareableContent cagrisi yavas olabiliyor
1202
+ // (TCC kontrolu + ekran/pencere envanteri). Ozellikle ekran
1203
+ // konfigurasyonu degistikten sonra (clamshell) bu envanter sifirdan
1204
+ // kuruluyor ve saniyeler surebiliyor.
1205
+ //
1206
+ // Sadece "isitmak" yetmiyordu: kayit aninda cagri YINE bastan
1207
+ // yapiliyordu. Artik sonucu saklayip kayitta yeniden kullaniyoruz,
1208
+ // yani bu adim tamamen atlanabiliyor.
1047
1209
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
1210
+ CFAbsoluteTime fetchStart = CFAbsoluteTimeGetCurrent();
1048
1211
  [SCShareableContent getShareableContentWithCompletionHandler:^(SCShareableContent *content, NSError *contentError) {
1049
- if (contentError) {
1212
+ if (contentError || !content) {
1050
1213
  MRLog(@"⚠️ Prewarm shareable content failed: %@", contentError.localizedDescription);
1051
1214
  return;
1052
1215
  }
1053
- MRLog(@"🔥 Prewarm: shareable content hazir (%lu ekran, %lu pencere)",
1216
+ SCKStoreShareableContent(content);
1217
+ NSLog(@"🔥 Prewarm: shareable content hazir ve saklandi (%.0fms, %lu ekran, %lu pencere)",
1218
+ (CFAbsoluteTimeGetCurrent() - fetchStart) * 1000.0,
1054
1219
  (unsigned long)content.displays.count,
1055
1220
  (unsigned long)content.windows.count);
1056
1221
  }];
@@ -1082,9 +1247,29 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
1082
1247
  return NO;
1083
1248
  }
1084
1249
 
1250
+ // Prewarm ile alinmis taze envanter varsa cagriyi TAMAMEN atla.
1251
+ // Bu cagri clamshell'de saniyeler surebiliyor ve kayit baslatmanin
1252
+ // en buyuk gecikme kalemi.
1253
+ if (@available(macOS 15.0, *)) {
1254
+ SCShareableContent *prewarmed = (SCShareableContent *)SCKTakeCachedShareableContent();
1255
+ if (prewarmed) {
1256
+ NSLog(@"⚡ Prewarmed shareable content kullanildi — envanter cagrisi atlandi");
1257
+ dispatch_async(controlQueue, ^{
1258
+ SCKPerformRecordingSetup(configCopy, prewarmed);
1259
+ });
1260
+ // NOT: Burada onbellegi tazelemek YOK.
1261
+ // Kayit setup'i calisirken yeni bir SCShareableContent istegi
1262
+ // baslatmak sistem envanterini kayitla ayni anda sorgular ve
1263
+ // kamera/stream baslatmayla yarisir. Tazeleme, kayit bittikten
1264
+ // sonra desktop tarafindaki periyodik isitmaya birakiliyor.
1265
+ return YES;
1266
+ }
1267
+ }
1268
+
1085
1269
  // CRITICAL FIX: Use dispatch_get_global_queue instead of main_queue
1086
1270
  // because Node.js standalone doesn't run macOS main event loop (only Electron does)
1087
1271
  NSLog(@"🚀 Requesting shareable content...");
1272
+ CFAbsoluteTime contentFetchStart = CFAbsoluteTimeGetCurrent();
1088
1273
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
1089
1274
  [SCShareableContent getShareableContentWithCompletionHandler:^(SCShareableContent *content, NSError *contentError) {
1090
1275
  if (contentError || !content) {
@@ -1092,7 +1277,8 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
1092
1277
  SCKFailScheduling();
1093
1278
  return;
1094
1279
  }
1095
- NSLog(@"✅ Got shareable content, starting recording setup...");
1280
+ NSLog(@"✅ Got shareable content in %.0fms, starting recording setup...",
1281
+ (CFAbsoluteTimeGetCurrent() - contentFetchStart) * 1000.0);
1096
1282
  dispatch_async(controlQueue, ^{
1097
1283
  SCKPerformRecordingSetup(configCopy, content);
1098
1284
  });
@@ -1422,31 +1608,41 @@ static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *c
1422
1608
  CGRect dispBounds = CGRectMake(disp.frame.origin.x, disp.frame.origin.y,
1423
1609
  disp.frame.size.width, disp.frame.size.height);
1424
1610
  if (CGRectContainsPoint(dispBounds, windowCenter)) {
1425
- NSInteger physW = (NSInteger)CGDisplayPixelsWide(disp.displayID);
1426
- NSInteger physH = (NSInteger)CGDisplayPixelsHigh(disp.displayID);
1427
- CGFloat scX = (disp.width > 0) ? (CGFloat)physW / (CGFloat)disp.width : 1.0;
1428
- CGFloat scY = (disp.height > 0) ? (CGFloat)physH / (CGFloat)disp.height : 1.0;
1429
- scaleFactor = MAX(scX, scY);
1611
+ scaleFactor = SCKBackingScaleForDisplay(disp);
1430
1612
  break;
1431
1613
  }
1432
1614
  }
1433
1615
  // Fallback: use main display scale factor
1434
1616
  if (scaleFactor == 1.0 && content.displays.count > 0) {
1435
1617
  SCDisplay *mainDisp = content.displays.firstObject;
1436
- NSInteger physW = (NSInteger)CGDisplayPixelsWide(mainDisp.displayID);
1437
- NSInteger physH = (NSInteger)CGDisplayPixelsHigh(mainDisp.displayID);
1438
- CGFloat scX = (mainDisp.width > 0) ? (CGFloat)physW / (CGFloat)mainDisp.width : 1.0;
1439
- CGFloat scY = (mainDisp.height > 0) ? (CGFloat)physH / (CGFloat)mainDisp.height : 1.0;
1440
- scaleFactor = MAX(scX, scY);
1618
+ scaleFactor = SCKBackingScaleForDisplay(mainDisp);
1441
1619
  }
1442
1620
 
1443
- NSInteger physicalWindowWidth = (NSInteger)(windowLogicalWidth * scaleFactor);
1444
- 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);
1445
1642
 
1446
1643
  MRLog(@"🪟 Recording window: %@ logical=%ux%u, physical=%ldx%ld, scale=%.2fx",
1447
1644
  targetWindow.title, (unsigned)windowLogicalWidth, (unsigned)windowLogicalHeight,
1448
1645
  (long)physicalWindowWidth, (long)physicalWindowHeight, scaleFactor);
1449
- filter = [[SCContentFilter alloc] initWithDesktopIndependentWindow:targetWindow];
1450
1646
  recordingWidth = physicalWindowWidth;
1451
1647
  recordingHeight = physicalWindowHeight;
1452
1648
  } else {
@@ -1482,22 +1678,36 @@ static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *c
1482
1678
  }
1483
1679
 
1484
1680
  if (targetDisplay) {
1485
- // Use physical pixel dimensions for Retina displays (not logical points)
1486
1681
  CGDirectDisplayID displayID = targetDisplay.displayID;
1487
- NSInteger physicalWidth = (NSInteger)CGDisplayPixelsWide(displayID);
1488
- NSInteger physicalHeight = (NSInteger)CGDisplayPixelsHigh(displayID);
1489
- NSInteger logicalWidth = targetDisplay.width;
1490
- NSInteger logicalHeight = targetDisplay.height;
1682
+ filter = [[SCContentFilter alloc] initWithDisplay:targetDisplay excludingWindows:@[]];
1491
1683
 
1492
- CGFloat scaleX = (logicalWidth > 0) ? (CGFloat)physicalWidth / (CGFloat)logicalWidth : 1.0;
1493
- CGFloat scaleY = (logicalHeight > 0) ? (CGFloat)physicalHeight / (CGFloat)logicalHeight : 1.0;
1494
- 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));
1495
1704
 
1496
- MRLog(@"🖥️ Recording display %u: logical=%dx%d, physical=%ldx%ld, scale=%.2fx",
1497
- 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,
1498
1709
  (long)physicalWidth, (long)physicalHeight, scaleFactor);
1499
1710
 
1500
- filter = [[SCContentFilter alloc] initWithDisplay:targetDisplay excludingWindows:@[]];
1501
1711
  recordingWidth = physicalWidth;
1502
1712
  recordingHeight = physicalHeight;
1503
1713
  } else {
@@ -1507,19 +1717,28 @@ static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *c
1507
1717
  }
1508
1718
  }
1509
1719
 
1510
- 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"]) {
1511
1727
  CGFloat cropWidth = [captureRect[@"width"] doubleValue];
1512
1728
  CGFloat cropHeight = [captureRect[@"height"] doubleValue];
1513
1729
  if (cropWidth > 0 && cropHeight > 0) {
1514
1730
  // Scale crop dimensions for Retina displays
1515
1731
  CGFloat cropScaleFactor = 1.0;
1516
1732
  if (targetDisplay) {
1517
- NSInteger physW = (NSInteger)CGDisplayPixelsWide(targetDisplay.displayID);
1518
- NSInteger physH = (NSInteger)CGDisplayPixelsHigh(targetDisplay.displayID);
1519
- CGFloat scX = (targetDisplay.width > 0) ? (CGFloat)physW / (CGFloat)targetDisplay.width : 1.0;
1520
- CGFloat scY = (targetDisplay.height > 0) ? (CGFloat)physH / (CGFloat)targetDisplay.height : 1.0;
1521
- 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
+ }
1522
1740
  }
1741
+ cropScaleFactor = MIN(4.0, MAX(1.0, cropScaleFactor));
1523
1742
  NSInteger physicalCropWidth = (NSInteger)(cropWidth * cropScaleFactor);
1524
1743
  NSInteger physicalCropHeight = (NSInteger)(cropHeight * cropScaleFactor);
1525
1744
  MRLog(@"🔲 Crop area: logical=%.0fx%.0f, physical=%ldx%ld, scale=%.2fx at (%.0f,%.0f)",
@@ -85,11 +85,31 @@ static bool shouldSkipSelectableWindowOwner(NSString *windowOwner) {
85
85
  }
86
86
 
87
87
  // Record icon helpers
88
- static NSImage *CreateRecordIconImage(CGFloat size) {
89
- const CGFloat leadingInset = 24.0;
90
- const CGFloat trailingSpacing = 6.0;
91
- CGFloat width = leadingInset + size + trailingSpacing;
92
- NSImage *image = [[[NSImage alloc] initWithSize:NSMakeSize(width, size)] autorelease];
88
+ // ---------------------------------------------------------------------------
89
+ // Marka butonu metrikleri — electron/main.cjs içindeki alan seçici `button.primary`
90
+ // ile bire bir: padding 14px 26px | min-width 220px | radius 18px | font 15px/650
91
+ // gap 10px | ikon 20px halka + 8px nokta | hover: translateY(-1px)
92
+ // ---------------------------------------------------------------------------
93
+ static const CGFloat kBrandFontSize = 15.0;
94
+ static const CGFloat kBrandCornerRadius = 18.0;
95
+ static const CGFloat kBrandPaddingX = 26.0;
96
+ static const CGFloat kBrandPaddingY = 16.0; // web 14px; NSButton'da metin
97
+ // biraz daha sıkışık durduğu için +2
98
+ static const CGFloat kBrandMinWidth = 220.0;
99
+ static const CGFloat kBrandIconDiameter = 20.0;
100
+ static const CGFloat kBrandIconSpacing = 10.0;
101
+ // Hover: web'de renk DEĞİŞMİYOR, buton 1px yukarı kalkıyor.
102
+ static const CGFloat kBrandHoverLift = 1.0;
103
+
104
+ // CSS açısı (0° = yukarı, saat yönü) -> NSGradient açısı (0° = sağa, saat yönü tersi)
105
+ static const CGFloat kBrandBaseAngle = 259.0; // CSS 190.98deg
106
+ static const CGFloat kBrandGlossAngle = 249.25; // CSS 200.71deg
107
+
108
+ // Alan seçicideki .primary-icon ile aynı: 20x20 halka, 2px beyaz kenarlık
109
+ // (%90 alfa), ortasında 8px beyaz nokta. Sağdaki boşluk CSS'teki gap:10px.
110
+ static NSImage *CreateRecordIconImage(CGFloat diameter, CGFloat trailingGap) {
111
+ CGFloat width = diameter + trailingGap;
112
+ NSImage *image = [[[NSImage alloc] initWithSize:NSMakeSize(width, diameter)] autorelease];
93
113
  if (!image) {
94
114
  return nil;
95
115
  }
@@ -97,26 +117,25 @@ static NSImage *CreateRecordIconImage(CGFloat size) {
97
117
  [image lockFocus];
98
118
 
99
119
  [[NSColor clearColor] setFill];
100
- NSRectFill(NSMakeRect(0, 0, width, size));
120
+ NSRectFill(NSMakeRect(0, 0, width, diameter));
101
121
 
102
- CGFloat strokeWidth = MAX(2.0, size * 0.12);
103
- NSRect iconRect = NSMakeRect(leadingInset,
104
- (size - size) / 2.0,
105
- size,
106
- size);
122
+ NSColor *iconColor = [NSColor colorWithSRGBRed:1.0 green:1.0 blue:1.0 alpha:0.9];
123
+
124
+ const CGFloat strokeWidth = 2.0;
125
+ NSRect iconRect = NSMakeRect(0, 0, diameter, diameter);
107
126
  NSRect outerRect = NSInsetRect(iconRect, strokeWidth / 2.0, strokeWidth / 2.0);
108
127
  NSBezierPath *outerPath = [NSBezierPath bezierPathWithOvalInRect:outerRect];
109
128
  [outerPath setLineWidth:strokeWidth];
110
- [[NSColor whiteColor] setStroke];
129
+ [iconColor setStroke];
111
130
  [outerPath stroke];
112
131
 
113
- CGFloat innerDiameter = size * 0.45;
132
+ const CGFloat innerDiameter = 8.0;
114
133
  NSRect innerRect = NSMakeRect(NSMidX(iconRect) - innerDiameter / 2.0,
115
134
  NSMidY(iconRect) - innerDiameter / 2.0,
116
135
  innerDiameter,
117
136
  innerDiameter);
118
137
  NSBezierPath *innerPath = [NSBezierPath bezierPathWithOvalInRect:innerRect];
119
- [[NSColor whiteColor] setFill];
138
+ [iconColor setFill];
120
139
  [innerPath fill];
121
140
 
122
141
  [image unlockFocus];
@@ -128,7 +147,7 @@ static NSImage *CreateRecordIconImage(CGFloat size) {
128
147
  static NSImage *GetStartRecordIcon(void) {
129
148
  static NSImage *recordIcon = nil;
130
149
  if (!recordIcon) {
131
- recordIcon = [CreateRecordIconImage(18.0) retain];
150
+ recordIcon = [CreateRecordIconImage(kBrandIconDiameter, kBrandIconSpacing) retain];
132
151
  }
133
152
  return recordIcon;
134
153
  }
@@ -146,15 +165,54 @@ static void ApplyStartRecordButtonIcon(NSButton *button) {
146
165
  [button setImage:icon];
147
166
  [button setImageScaling:NSImageScaleNone];
148
167
  [button setImagePosition:NSImageLeft];
168
+ // İç boşluk ApplyBrandButtonStyle'da ayarlanıyor (padding 14px 26px).
169
+ }
149
170
 
150
- if ([button respondsToSelector:@selector(setContentInsets:)]) {
151
- NSEdgeInsets insets = NSEdgeInsetsMake(0, 12.0, 0, 12.0);
152
- [button setContentInsets:insets];
171
+ // ---------------------------------------------------------------------------
172
+ // Marka butonu pages/selection.vue içindeki .confirm-button ile aynı görünüm
173
+ //
174
+ // background: linear-gradient(200.71deg, rgba(255,255,255,.57) -0.3%,
175
+ // rgba(255,255,255,0) 70.49%),
176
+ // linear-gradient(190.98deg, #006aff 26.77%, #0030ff);
177
+ // color: #fff | padding: 8px 16px | border-radius: 6px | font-size: 14px
178
+ // hover: filter: brightness(0.92) | transition: 0.2s
179
+ //
180
+ // Arka plan drawRect'te çizilir: CAGradientLayer sublayer olarak eklenince
181
+ // butonun başlığı ve ikonu gradient'in ALTINDA kalıyor.
182
+ // ---------------------------------------------------------------------------
183
+ static NSFont *BrandButtonFont(void) {
184
+ // CSS: font-size 15px, font-weight 650 (semibold ile bire bir yakın).
185
+ NSFont *inter = [NSFont fontWithName:@"Inter-SemiBold" size:kBrandFontSize];
186
+ if (inter) return inter;
187
+ return [NSFont systemFontOfSize:kBrandFontSize weight:NSFontWeightSemibold];
188
+ }
189
+
190
+ static NSGradient *BrandBaseGradient(void) {
191
+ static NSGradient *gradient = nil;
192
+ if (!gradient) {
193
+ NSColor *from = [NSColor colorWithSRGBRed:0.0 green:106.0 / 255.0 blue:1.0 alpha:1.0];
194
+ NSColor *to = [NSColor colorWithSRGBRed:0.0 green:48.0 / 255.0 blue:1.0 alpha:1.0];
195
+ gradient = [[NSGradient alloc] initWithColorsAndLocations:from, 0.2677, to, 1.0, nil];
153
196
  }
197
+ return gradient;
198
+ }
154
199
 
155
- if ([button respondsToSelector:@selector(setContentTintColor:)]) {
156
- [button setContentTintColor:[NSColor whiteColor]];
200
+ static NSGradient *BrandGlossGradient(void) {
201
+ static NSGradient *gradient = nil;
202
+ if (!gradient) {
203
+ NSColor *from = [NSColor colorWithSRGBRed:1.0 green:1.0 blue:1.0 alpha:0.57];
204
+ NSColor *to = [NSColor colorWithSRGBRed:1.0 green:1.0 blue:1.0 alpha:0.0];
205
+ gradient = [[NSGradient alloc] initWithColorsAndLocations:from, 0.0, to, 0.7049, nil];
157
206
  }
207
+ return gradient;
208
+ }
209
+
210
+ static void DrawBrandBackground(NSRect bounds) {
211
+ NSBezierPath *path = [NSBezierPath bezierPathWithRoundedRect:bounds
212
+ xRadius:kBrandCornerRadius
213
+ yRadius:kBrandCornerRadius];
214
+ [BrandBaseGradient() drawInBezierPath:path angle:kBrandBaseAngle];
215
+ [BrandGlossGradient() drawInBezierPath:path angle:kBrandGlossAngle];
158
216
  }
159
217
 
160
218
  // CGWindow (sol-üst orijin) <-> Cocoa (sol-alt orijin) dönüşümünde referans yükseklik
@@ -205,6 +263,10 @@ void updateScreenOverlays();
205
263
  // Custom button with hover effects
206
264
  @interface HoverButton : NSButton
207
265
  @property (nonatomic) BOOL isHovered;
266
+ // Marka butonu (alan seçimindeki mavi buton) görünümü: arka plan drawRect'te
267
+ // çizilir. CAGradientLayer sublayer olarak eklenirse butonun kendi çizimi
268
+ // (başlık + ikon) ALTINDA kalıyor, o yüzden katman değil çizim kullanılıyor.
269
+ @property (nonatomic) BOOL usesBrandStyle;
208
270
  - (void)setupHoverTracking;
209
271
  @end
210
272
 
@@ -368,7 +430,19 @@ void updateScreenOverlays();
368
430
  - (void)mouseEntered:(NSEvent *)event {
369
431
  self.isHovered = YES;
370
432
  [[NSCursor pointingHandCursor] set];
371
-
433
+
434
+ // Marka butonu: hover'da web ile aynı şekilde HAFİF KOYULAŞIR
435
+ // (filter: brightness(0.92)). Gradient katmanlı olduğu için backgroundColor
436
+ // ile oynanamaz, örtü katmanının opacity'si kullanılır.
437
+ if (self.usesBrandStyle) {
438
+ // CSS: transform: translateY(-1px) — renk değişmiyor, buton kalkıyor.
439
+ [CATransaction begin];
440
+ [CATransaction setAnimationDuration:0.11];
441
+ self.layer.transform = CATransform3DMakeTranslation(0, kBrandHoverLift, 0);
442
+ [CATransaction commit];
443
+ return;
444
+ }
445
+
372
446
  // Brighten background on hover
373
447
  if (self.layer.backgroundColor) {
374
448
  CGFloat red, green, blue, alpha;
@@ -388,7 +462,15 @@ void updateScreenOverlays();
388
462
  - (void)mouseExited:(NSEvent *)event {
389
463
  self.isHovered = NO;
390
464
  [[NSCursor arrowCursor] set];
391
-
465
+
466
+ if (self.usesBrandStyle) {
467
+ [CATransaction begin];
468
+ [CATransaction setAnimationDuration:0.11];
469
+ self.layer.transform = CATransform3DIdentity;
470
+ [CATransaction commit];
471
+ return;
472
+ }
473
+
392
474
  // Restore original background color
393
475
  NSString *title = [self title];
394
476
  if ([title isEqualToString:@"Start Record"]) {
@@ -412,18 +494,100 @@ void updateScreenOverlays();
412
494
 
413
495
  - (void)updateTrackingAreas {
414
496
  [super updateTrackingAreas];
415
-
497
+
416
498
  // Remove old tracking areas
417
499
  for (NSTrackingArea *area in self.trackingAreas) {
418
500
  [self removeTrackingArea:area];
419
501
  }
420
-
502
+
421
503
  // Add new tracking area
422
504
  [self setupHoverTracking];
423
505
  }
424
506
 
507
+ // Marka arka planı drawRect'te çizildiği için başlık ve ikon her zaman ÜSTTE kalır.
508
+ - (void)drawRect:(NSRect)dirtyRect {
509
+ if (self.usesBrandStyle) {
510
+ DrawBrandBackground([self bounds]);
511
+ }
512
+ [super drawRect:dirtyRect];
513
+ }
514
+
425
515
  @end
426
516
 
517
+ // Marka butonu stilini uygular: alan seçimindeki .confirm-button ile aynı
518
+ // tipografi, köşe ve iç boşluk. Arka planı HoverButton.drawRect çiziyor.
519
+ static void ApplyBrandButtonStyle(NSButton *button) {
520
+ if (!button) return;
521
+
522
+ [button setWantsLayer:YES];
523
+ [button setBordered:NO];
524
+ [button setFocusRingType:NSFocusRingTypeNone];
525
+ [button setShowsBorderOnlyWhileMouseInside:NO];
526
+
527
+ if ([button isKindOfClass:[HoverButton class]]) {
528
+ [(HoverButton *)button setUsesBrandStyle:YES];
529
+ }
530
+
531
+ CALayer *root = [button layer];
532
+ if (root) {
533
+ // Arka plan drawRect'te; katman tarafında hiçbir dolgu/kenar kalmasın.
534
+ root.backgroundColor = [[NSColor clearColor] CGColor];
535
+ root.borderWidth = 0.0;
536
+ root.borderColor = [[NSColor clearColor] CGColor];
537
+ root.shadowOpacity = 0.0;
538
+ root.shadowRadius = 0.0;
539
+ root.shadowOffset = CGSizeMake(0, 0);
540
+ root.masksToBounds = NO;
541
+ }
542
+
543
+ NSFont *font = BrandButtonFont();
544
+ [button setFont:font];
545
+
546
+ // İç boşluk web ile aynı: padding 14px 26px.
547
+ if ([button respondsToSelector:@selector(setContentInsets:)]) {
548
+ [button setContentInsets:NSEdgeInsetsMake(kBrandPaddingY, kBrandPaddingX,
549
+ kBrandPaddingY, kBrandPaddingX)];
550
+ }
551
+ // Web'de ikon ve metin bir arada ortalanıyor (inline-flex + center).
552
+ // Bu olmadan NSButton ikonu sol kenara dayar, metni ortada bırakır.
553
+ if ([button respondsToSelector:@selector(setImageHugsTitle:)]) {
554
+ [button setImageHugsTitle:YES];
555
+ }
556
+
557
+ NSString *title = [button title] ?: @"";
558
+ NSMutableParagraphStyle *paragraph = [[NSMutableParagraphStyle alloc] init];
559
+ [paragraph setAlignment:NSTextAlignmentCenter];
560
+ NSDictionary *attributes = @{
561
+ NSForegroundColorAttributeName : [NSColor whiteColor],
562
+ NSParagraphStyleAttributeName : paragraph,
563
+ NSFontAttributeName : font
564
+ };
565
+ NSMutableAttributedString *attributed =
566
+ [[NSMutableAttributedString alloc] initWithString:title];
567
+ [attributed addAttributes:attributes range:NSMakeRange(0, [attributed length])];
568
+ [button setAttributedTitle:attributed];
569
+
570
+ // Boyut: içerik + padding, en az min-width 220px (web ile aynı).
571
+ NSSize textSize = [title sizeWithAttributes:attributes];
572
+ CGFloat contentWidth = ceil(textSize.width);
573
+ CGFloat contentHeight = ceil(textSize.height);
574
+ NSImage *icon = [button image];
575
+ if (icon) {
576
+ // İkon görüntüsü sağındaki gap'i (10px) zaten içeriyor.
577
+ contentWidth += [icon size].width;
578
+ contentHeight = MAX(contentHeight, [icon size].height);
579
+ }
580
+ NSRect frame = [button frame];
581
+ frame.size.width = MAX(kBrandMinWidth, contentWidth + kBrandPaddingX * 2.0);
582
+ frame.size.height = contentHeight + kBrandPaddingY * 2.0;
583
+ [button setFrame:frame];
584
+ [button setNeedsDisplay:YES];
585
+
586
+ [attributed release];
587
+ [paragraph release];
588
+ }
589
+
590
+
427
591
  @implementation NoFocusWindow
428
592
 
429
593
  - (BOOL)canBecomeKeyWindow {
@@ -1207,28 +1371,10 @@ void updateOverlay() {
1207
1371
  [targetSelectButton setBordered:NO];
1208
1372
  [targetSelectButton setFont:[NSFont systemFontOfSize:16 weight:NSFontWeightRegular]];
1209
1373
 
1210
- // CRITICAL VISUAL FIX: Super aggressive styling for external displays
1211
- [targetSelectButton setWantsLayer:YES];
1212
- [targetSelectButton.layer setBackgroundColor:[[NSColor redColor] CGColor]]; // BRIGHT RED for testing
1213
- [targetSelectButton.layer setCornerRadius:8.0];
1214
- [targetSelectButton.layer setBorderWidth:3.0]; // THICK border
1215
- [targetSelectButton.layer setBorderColor:[[NSColor yellowColor] CGColor]]; // YELLOW border
1216
- [targetSelectButton.layer setMasksToBounds:YES];
1217
-
1218
- // Force very visible styling
1219
- [targetSelectButton.layer setShadowColor:[[NSColor blackColor] CGColor]];
1220
- [targetSelectButton.layer setShadowOffset:CGSizeMake(5, 5)];
1221
- [targetSelectButton.layer setShadowRadius:10.0];
1222
- [targetSelectButton.layer setShadowOpacity:1.0];
1223
-
1224
- // Clean white text - normal weight
1225
- NSMutableAttributedString *titleString = [[NSMutableAttributedString alloc]
1226
- initWithString:[targetSelectButton title]];
1227
- [titleString addAttribute:NSForegroundColorAttributeName
1228
- value:[NSColor whiteColor]
1229
- range:NSMakeRange(0, [titleString length])];
1230
- [targetSelectButton setAttributedTitle:titleString];
1231
-
1374
+ // Alan seçimindeki mavi buton ile aynı marka görünümü
1375
+ ApplyStartRecordButtonIcon(targetSelectButton);
1376
+ ApplyBrandButtonStyle(targetSelectButton);
1377
+
1232
1378
  // Set target and action
1233
1379
  if (!g_delegate) {
1234
1380
  g_delegate = [[WindowSelectorDelegate alloc] init];
@@ -1244,7 +1390,10 @@ void updateOverlay() {
1244
1390
  NSLog(@"🆕 Added new button to Screen %ld overlay - total subviews now: %lu", targetScreenIndex, [targetOverlay.contentView subviews].count);
1245
1391
  }
1246
1392
 
1393
+ // Butonun her gösterimde marka metriklerinde kalmasını garanti et
1394
+ // (mevcut buton yeniden kullanılıyor olabilir).
1247
1395
  ApplyStartRecordButtonIcon(targetSelectButton);
1396
+ ApplyBrandButtonStyle(targetSelectButton);
1248
1397
 
1249
1398
  // Position buttons - Start Record in center of selected window
1250
1399
  if (targetSelectButton) {
@@ -1614,15 +1763,17 @@ void updateScreenOverlays() {
1614
1763
  if ([subview isKindOfClass:[NSButton class]]) {
1615
1764
  NSButton *button = (NSButton *)subview;
1616
1765
  if ([button.title isEqualToString:@"Start Record"]) {
1617
- if (isActiveScreen) {
1618
- // Active screen: bright, prominent button with new RGB color
1619
- [button.layer setBackgroundColor:[[NSColor colorWithRed:77.0/255.0 green:30.0/255.0 blue:231.0/255.0 alpha:1.0] CGColor]];
1620
- [button setAlphaValue:1.0];
1621
- } else {
1622
- // Inactive screen: dimmer button with new RGB color
1623
- [button.layer setBackgroundColor:[[NSColor colorWithRed:77.0/255.0 green:30.0/255.0 blue:231.0/255.0 alpha:0.6] CGColor]];
1624
- [button setAlphaValue:0.7];
1766
+ // Marka butonunun arka planı drawRect'te çiziliyor;
1767
+ // layer.backgroundColor set edilirse hapın altında
1768
+ // köşeli bir dikdörtgen olarak görünüyor.
1769
+ BOOL usesBrand = [button isKindOfClass:[HoverButton class]] &&
1770
+ [(HoverButton *)button usesBrandStyle];
1771
+ if (!usesBrand) {
1772
+ [button.layer setBackgroundColor:
1773
+ [[NSColor colorWithRed:77.0/255.0 green:30.0/255.0 blue:231.0/255.0
1774
+ alpha:(isActiveScreen ? 1.0 : 0.6)] CGColor]];
1625
1775
  }
1776
+ [button setAlphaValue:isActiveScreen ? 1.0 : 0.7];
1626
1777
  }
1627
1778
  }
1628
1779
  if ([subview isKindOfClass:[NSTextField class]]) {
@@ -1827,29 +1978,11 @@ bool startScreenSelection() {
1827
1978
  [selectButton setFont:[NSFont systemFontOfSize:16 weight:NSFontWeightRegular]];
1828
1979
  [selectButton setTag:i]; // Set screen index as tag
1829
1980
 
1830
- // Modern button styling with new RGB color
1831
- [selectButton setWantsLayer:YES];
1832
- [selectButton.layer setBackgroundColor:[[NSColor colorWithRed:90.0/255.0 green:50.0/255.0 blue:250.0/255.0 alpha:1.0] CGColor]];
1833
- [selectButton.layer setCornerRadius:8.0];
1834
- [selectButton.layer setBorderWidth:0.0];
1835
-
1836
- // Remove all button borders and decorations
1837
- [selectButton.layer setShadowOpacity:0.0];
1838
- [selectButton.layer setShadowRadius:0.0];
1839
- [selectButton.layer setShadowOffset:NSMakeSize(0, 0)];
1840
- [selectButton.layer setMasksToBounds:YES];
1841
-
1842
- // Clean white text - normal weight
1843
- [selectButton setFont:[NSFont systemFontOfSize:16 weight:NSFontWeightRegular]];
1981
+ // Alan seçimindeki mavi buton ile aynı marka görünümü
1844
1982
  [selectButton setTitle:@"Start Record"];
1845
- NSMutableAttributedString *titleString = [[NSMutableAttributedString alloc]
1846
- initWithString:[selectButton title]];
1847
- [titleString addAttribute:NSForegroundColorAttributeName
1848
- value:[NSColor whiteColor]
1849
- range:NSMakeRange(0, [titleString length])];
1850
- [selectButton setAttributedTitle:titleString];
1851
-
1852
1983
  ApplyStartRecordButtonIcon(selectButton);
1984
+ ApplyBrandButtonStyle(selectButton);
1985
+
1853
1986
 
1854
1987
  // Clean button - no shadows or highlights
1855
1988
 
@@ -2314,29 +2447,10 @@ Napi::Value StartWindowSelection(const Napi::CallbackInfo& info) {
2314
2447
  [g_selectButton setBordered:NO];
2315
2448
  [g_selectButton setFont:[NSFont systemFontOfSize:16 weight:NSFontWeightRegular]];
2316
2449
 
2317
- // Modern button styling with new RGB color
2318
- [g_selectButton setWantsLayer:YES];
2319
- [g_selectButton.layer setBackgroundColor:[[NSColor colorWithRed:90.0/255.0 green:50.0/255.0 blue:250.0/255.0 alpha:1.0] CGColor]];
2320
- [g_selectButton.layer setCornerRadius:8.0];
2321
- [g_selectButton.layer setBorderWidth:0.0];
2322
-
2323
- // Remove all button borders and decorations
2324
- [g_selectButton.layer setShadowOpacity:0.0];
2325
- [g_selectButton.layer setShadowRadius:0.0];
2326
- [g_selectButton.layer setShadowOffset:NSMakeSize(0, 0)];
2327
- [g_selectButton.layer setMasksToBounds:YES];
2328
- [g_selectButton.layer setBorderWidth:0.0];
2329
- [g_selectButton.layer setBorderColor:[[NSColor clearColor] CGColor]];
2330
-
2331
- // Clean white text - normal weight
2332
- NSMutableAttributedString *titleString = [[NSMutableAttributedString alloc]
2333
- initWithString:[g_selectButton title]];
2334
- [titleString addAttribute:NSForegroundColorAttributeName
2335
- value:[NSColor whiteColor]
2336
- range:NSMakeRange(0, [titleString length])];
2337
- [g_selectButton setAttributedTitle:titleString];
2338
-
2450
+ // Alan seçimindeki mavi buton ile aynı marka görünümü
2339
2451
  ApplyStartRecordButtonIcon(g_selectButton);
2452
+ ApplyBrandButtonStyle(g_selectButton);
2453
+
2340
2454
 
2341
2455
  // Create delegate for button action and timer
2342
2456
  g_delegate = [[WindowSelectorDelegate alloc] init];