node-mac-recorder 2.24.5 → 2.24.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +19 -2
- package/package.json +1 -1
- package/src/mac_recorder.mm +25 -0
- package/src/screen_capture_kit.mm +207 -56
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,19 @@ class MacRecorder extends EventEmitter {
|
|
|
1412
1416
|
};
|
|
1413
1417
|
}
|
|
1414
1418
|
|
|
1419
|
+
/**
|
|
1420
|
+
* Encoder'ın gerçek zamanlı yetişip yetişmediğini gösteren kare sayaçları.
|
|
1421
|
+
* Kayıt sırasında canlı, bittikten sonra son oturumun değerlerini döndürür.
|
|
1422
|
+
* dropRatio > 0.02 ise çözünürlük x FPS x bitrate bu makine için ağırdır ve
|
|
1423
|
+
* görüntü kalitesi sessizce düşüyor demektir.
|
|
1424
|
+
*/
|
|
1425
|
+
getCaptureFrameStats() {
|
|
1426
|
+
if (typeof nativeBinding.getCaptureFrameStats !== "function") {
|
|
1427
|
+
return { appendedFrames: 0, droppedFrames: 0, targetFps: 0, dropRatio: 0 };
|
|
1428
|
+
}
|
|
1429
|
+
return nativeBinding.getCaptureFrameStats();
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1415
1432
|
/**
|
|
1416
1433
|
* macOS'ta kayıt izinlerini kontrol eder
|
|
1417
1434
|
*/
|
package/package.json
CHANGED
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
|
|
|
@@ -1490,31 +1608,41 @@ static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *c
|
|
|
1490
1608
|
CGRect dispBounds = CGRectMake(disp.frame.origin.x, disp.frame.origin.y,
|
|
1491
1609
|
disp.frame.size.width, disp.frame.size.height);
|
|
1492
1610
|
if (CGRectContainsPoint(dispBounds, windowCenter)) {
|
|
1493
|
-
|
|
1494
|
-
NSInteger physH = (NSInteger)CGDisplayPixelsHigh(disp.displayID);
|
|
1495
|
-
CGFloat scX = (disp.width > 0) ? (CGFloat)physW / (CGFloat)disp.width : 1.0;
|
|
1496
|
-
CGFloat scY = (disp.height > 0) ? (CGFloat)physH / (CGFloat)disp.height : 1.0;
|
|
1497
|
-
scaleFactor = MAX(scX, scY);
|
|
1611
|
+
scaleFactor = SCKBackingScaleForDisplay(disp);
|
|
1498
1612
|
break;
|
|
1499
1613
|
}
|
|
1500
1614
|
}
|
|
1501
1615
|
// Fallback: use main display scale factor
|
|
1502
1616
|
if (scaleFactor == 1.0 && content.displays.count > 0) {
|
|
1503
1617
|
SCDisplay *mainDisp = content.displays.firstObject;
|
|
1504
|
-
|
|
1505
|
-
NSInteger physH = (NSInteger)CGDisplayPixelsHigh(mainDisp.displayID);
|
|
1506
|
-
CGFloat scX = (mainDisp.width > 0) ? (CGFloat)physW / (CGFloat)mainDisp.width : 1.0;
|
|
1507
|
-
CGFloat scY = (mainDisp.height > 0) ? (CGFloat)physH / (CGFloat)mainDisp.height : 1.0;
|
|
1508
|
-
scaleFactor = MAX(scX, scY);
|
|
1618
|
+
scaleFactor = SCKBackingScaleForDisplay(mainDisp);
|
|
1509
1619
|
}
|
|
1510
1620
|
|
|
1511
|
-
|
|
1512
|
-
|
|
1621
|
+
filter = [[SCContentFilter alloc] initWithDesktopIndependentWindow:targetWindow];
|
|
1622
|
+
if (@available(macOS 14.0, *)) {
|
|
1623
|
+
// ScreenCaptureKit exposes the authoritative point→pixel
|
|
1624
|
+
// conversion for this exact filter. Prefer it over display
|
|
1625
|
+
// heuristics (scaled modes and windows spanning displays).
|
|
1626
|
+
CGFloat filterScale = filter.pointPixelScale;
|
|
1627
|
+
if (isfinite(filterScale) && filterScale >= 1.0 && filterScale <= 4.0) {
|
|
1628
|
+
// desktopIndependentWindow filtresi bazı macOS/sürüm
|
|
1629
|
+
// kombinasyonlarında Retina pencerede bile 1.0 dönebiliyor.
|
|
1630
|
+
// Display geometrisinden bulunan 2x değeri 1x'e indirirsek
|
|
1631
|
+
// 1500x960 pencere gerçek 3000x1920 yerine logical boyutta
|
|
1632
|
+
// kaydediliyor ve zoom'da metin detayı geri dönülemez
|
|
1633
|
+
// biçimde kayboluyor. Filter yalnızca daha yüksek/güvenli
|
|
1634
|
+
// bir backing scale bildiriyorsa heuristiği yükseltsin.
|
|
1635
|
+
scaleFactor = MAX(scaleFactor, filterScale);
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
scaleFactor = MIN(4.0, MAX(1.0, scaleFactor));
|
|
1639
|
+
|
|
1640
|
+
NSInteger physicalWindowWidth = (NSInteger)llround(windowLogicalWidth * scaleFactor);
|
|
1641
|
+
NSInteger physicalWindowHeight = (NSInteger)llround(windowLogicalHeight * scaleFactor);
|
|
1513
1642
|
|
|
1514
1643
|
MRLog(@"🪟 Recording window: %@ logical=%ux%u, physical=%ldx%ld, scale=%.2fx",
|
|
1515
1644
|
targetWindow.title, (unsigned)windowLogicalWidth, (unsigned)windowLogicalHeight,
|
|
1516
1645
|
(long)physicalWindowWidth, (long)physicalWindowHeight, scaleFactor);
|
|
1517
|
-
filter = [[SCContentFilter alloc] initWithDesktopIndependentWindow:targetWindow];
|
|
1518
1646
|
recordingWidth = physicalWindowWidth;
|
|
1519
1647
|
recordingHeight = physicalWindowHeight;
|
|
1520
1648
|
} else {
|
|
@@ -1550,22 +1678,36 @@ static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *c
|
|
|
1550
1678
|
}
|
|
1551
1679
|
|
|
1552
1680
|
if (targetDisplay) {
|
|
1553
|
-
// Use physical pixel dimensions for Retina displays (not logical points)
|
|
1554
1681
|
CGDirectDisplayID displayID = targetDisplay.displayID;
|
|
1555
|
-
|
|
1556
|
-
NSInteger physicalHeight = (NSInteger)CGDisplayPixelsHigh(displayID);
|
|
1557
|
-
NSInteger logicalWidth = targetDisplay.width;
|
|
1558
|
-
NSInteger logicalHeight = targetDisplay.height;
|
|
1682
|
+
filter = [[SCContentFilter alloc] initWithDisplay:targetDisplay excludingWindows:@[]];
|
|
1559
1683
|
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1684
|
+
// CGDisplayPixelsWide/High reports the current Quartz coordinate
|
|
1685
|
+
// resolution on scaled Retina modes (for example 1800x1169), not
|
|
1686
|
+
// necessarily the backing pixels ScreenCaptureKit can deliver.
|
|
1687
|
+
// The filter exposes the authoritative point -> pixel scale and
|
|
1688
|
+
// content rect, so derive the requested stream size from those.
|
|
1689
|
+
CGFloat logicalWidth = targetDisplay.frame.size.width;
|
|
1690
|
+
CGFloat logicalHeight = targetDisplay.frame.size.height;
|
|
1691
|
+
CGFloat scaleFactor = SCKBackingScaleForDisplay(targetDisplay);
|
|
1692
|
+
if (@available(macOS 14.0, *)) {
|
|
1693
|
+
CGRect filterRect = filter.contentRect;
|
|
1694
|
+
CGFloat filterScale = filter.pointPixelScale;
|
|
1695
|
+
if (filterRect.size.width > 0 && filterRect.size.height > 0) {
|
|
1696
|
+
logicalWidth = filterRect.size.width;
|
|
1697
|
+
logicalHeight = filterRect.size.height;
|
|
1698
|
+
}
|
|
1699
|
+
if (isfinite(filterScale) && filterScale >= 1.0 && filterScale <= 4.0) {
|
|
1700
|
+
scaleFactor = MAX(scaleFactor, filterScale);
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
scaleFactor = MIN(4.0, MAX(1.0, scaleFactor));
|
|
1563
1704
|
|
|
1564
|
-
|
|
1565
|
-
|
|
1705
|
+
NSInteger physicalWidth = (NSInteger)llround(logicalWidth * scaleFactor);
|
|
1706
|
+
NSInteger physicalHeight = (NSInteger)llround(logicalHeight * scaleFactor);
|
|
1707
|
+
MRLog(@"🖥️ Recording display %u: logical=%.0fx%.0f, physical=%ldx%ld, scale=%.2fx (filter-backed)",
|
|
1708
|
+
displayID, logicalWidth, logicalHeight,
|
|
1566
1709
|
(long)physicalWidth, (long)physicalHeight, scaleFactor);
|
|
1567
1710
|
|
|
1568
|
-
filter = [[SCContentFilter alloc] initWithDisplay:targetDisplay excludingWindows:@[]];
|
|
1569
1711
|
recordingWidth = physicalWidth;
|
|
1570
1712
|
recordingHeight = physicalHeight;
|
|
1571
1713
|
} else {
|
|
@@ -1575,19 +1717,28 @@ static void SCKPerformRecordingSetup(NSDictionary *config, SCShareableContent *c
|
|
|
1575
1717
|
}
|
|
1576
1718
|
}
|
|
1577
1719
|
|
|
1578
|
-
|
|
1720
|
+
// desktopIndependentWindow kendi içerik sınırını zaten uygular. JS
|
|
1721
|
+
// katmanındaki window captureArea cursor metadata'sı olarak tutulabilir;
|
|
1722
|
+
// burada tekrar crop boyutu gibi kullanılırsa yukarıda hesaplanan Retina
|
|
1723
|
+
// physicalWindowWidth/Height logical 1x ölçülerle ezilir. Bu blok yalnızca
|
|
1724
|
+
// gerçek display/area kayıtlarında çıktı ölçüsünü değiştirmeli.
|
|
1725
|
+
BOOL isWindowCapture = windowId && [windowId integerValue] != 0;
|
|
1726
|
+
if (!isWindowCapture && captureRect && captureRect[@"width"] && captureRect[@"height"]) {
|
|
1579
1727
|
CGFloat cropWidth = [captureRect[@"width"] doubleValue];
|
|
1580
1728
|
CGFloat cropHeight = [captureRect[@"height"] doubleValue];
|
|
1581
1729
|
if (cropWidth > 0 && cropHeight > 0) {
|
|
1582
1730
|
// Scale crop dimensions for Retina displays
|
|
1583
1731
|
CGFloat cropScaleFactor = 1.0;
|
|
1584
1732
|
if (targetDisplay) {
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1733
|
+
cropScaleFactor = SCKBackingScaleForDisplay(targetDisplay);
|
|
1734
|
+
if (@available(macOS 14.0, *)) {
|
|
1735
|
+
CGFloat filterScale = filter.pointPixelScale;
|
|
1736
|
+
if (isfinite(filterScale) && filterScale >= 1.0 && filterScale <= 4.0) {
|
|
1737
|
+
cropScaleFactor = MAX(cropScaleFactor, filterScale);
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1590
1740
|
}
|
|
1741
|
+
cropScaleFactor = MIN(4.0, MAX(1.0, cropScaleFactor));
|
|
1591
1742
|
NSInteger physicalCropWidth = (NSInteger)(cropWidth * cropScaleFactor);
|
|
1592
1743
|
NSInteger physicalCropHeight = (NSInteger)(cropHeight * cropScaleFactor);
|
|
1593
1744
|
MRLog(@"🔲 Crop area: logical=%.0fx%.0f, physical=%ldx%ld, scale=%.2fx at (%.0f,%.0f)",
|