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 +44 -2
- package/package.json +1 -1
- package/src/mac_recorder.mm +25 -0
- package/src/screen_capture_kit.mm +238 -62
- package/src/window_selector.mm +75 -0
package/index.js
CHANGED
|
@@ -835,8 +835,12 @@ class MacRecorder extends EventEmitter {
|
|
|
835
835
|
};
|
|
836
836
|
}
|
|
837
837
|
|
|
838
|
-
//
|
|
839
|
-
|
|
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
package/src/mac_recorder.mm
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
296
|
+
double *bppOut,
|
|
240
297
|
NSInteger *minOut,
|
|
241
298
|
NSInteger *maxOut) {
|
|
242
299
|
NSString *normalized = SCKNormalizeQualityPreset(preset);
|
|
243
300
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
310
|
+
bpp = 0.09;
|
|
250
311
|
minBitrate = 10 * 1000 * 1000;
|
|
251
312
|
maxBitrate = 45 * 1000 * 1000;
|
|
252
313
|
} else if ([normalized isEqualToString:@"medium"]) {
|
|
253
|
-
|
|
254
|
-
minBitrate =
|
|
255
|
-
maxBitrate =
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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 (
|
|
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,
|
|
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: @(
|
|
840
|
-
|
|
841
|
-
|
|
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: @(
|
|
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
|
-
|
|
1494
|
-
|
|
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
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
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
|
-
|
|
1565
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
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
|
-
//
|
|
1632
|
-
|
|
1633
|
-
//
|
|
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
|
package/src/window_selector.mm
CHANGED
|
@@ -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));
|