node-mac-recorder 2.21.20 → 2.21.22
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/.claude/settings.local.json +5 -1
- package/index.js +1 -0
- package/package.json +1 -1
- package/src/audio_recorder.mm +56 -9
- package/src/avfoundation_recorder.mm +17 -3
- package/src/camera_recorder.mm +72 -15
- package/src/mac_recorder.mm +6 -1
- package/src/screen_capture_kit.mm +47 -5
|
@@ -5,7 +5,11 @@
|
|
|
5
5
|
"Bash(chmod:*)",
|
|
6
6
|
"Bash(node test-sync.js:*)",
|
|
7
7
|
"Bash(node:*)",
|
|
8
|
-
"Bash(ALLOW_CONTINUITY_CAMERA=1 node:*)"
|
|
8
|
+
"Bash(ALLOW_CONTINUITY_CAMERA=1 node:*)",
|
|
9
|
+
"Bash(awk:*)",
|
|
10
|
+
"Bash(ffprobe:*)",
|
|
11
|
+
"Bash(sw_vers:*)",
|
|
12
|
+
"Bash(system_profiler:*)"
|
|
9
13
|
],
|
|
10
14
|
"deny": [],
|
|
11
15
|
"ask": []
|
package/index.js
CHANGED
|
@@ -103,6 +103,7 @@ class MacRecorder extends EventEmitter {
|
|
|
103
103
|
position: device?.position ?? "unspecified",
|
|
104
104
|
transportType: device?.transportType ?? null,
|
|
105
105
|
isConnected: device?.isConnected ?? false,
|
|
106
|
+
isDefault: device?.isDefault === true,
|
|
106
107
|
hasFlash: device?.hasFlash ?? false,
|
|
107
108
|
supportsDepth: device?.supportsDepth ?? false,
|
|
108
109
|
deviceType: device?.deviceType ?? null,
|
package/package.json
CHANGED
package/src/audio_recorder.mm
CHANGED
|
@@ -224,10 +224,30 @@ static dispatch_queue_t g_audioCaptureQueue = nil;
|
|
|
224
224
|
return YES;
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
-
|
|
227
|
+
// CRITICAL FIX: For external devices (especially Continuity Microphone),
|
|
228
|
+
// stopRunning can hang if device is disconnected. Use async approach.
|
|
229
|
+
MRLog(@"🛑 AudioRecorder: Stopping session (external device safe)...");
|
|
230
|
+
|
|
231
|
+
// Stop session on background thread to avoid blocking
|
|
232
|
+
AVCaptureSession *sessionToStop = self.session;
|
|
233
|
+
AVCaptureAudioDataOutput *outputToStop = self.audioOutput;
|
|
234
|
+
|
|
235
|
+
// Clear references FIRST to prevent new samples
|
|
228
236
|
self.session = nil;
|
|
229
237
|
self.audioOutput = nil;
|
|
230
238
|
|
|
239
|
+
// Stop session asynchronously with timeout protection
|
|
240
|
+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
|
|
241
|
+
@autoreleasepool {
|
|
242
|
+
if ([sessionToStop isRunning]) {
|
|
243
|
+
MRLog(@"🛑 Stopping AVCaptureSession...");
|
|
244
|
+
[sessionToStop stopRunning];
|
|
245
|
+
MRLog(@"✅ AVCaptureSession stopped");
|
|
246
|
+
}
|
|
247
|
+
// Release happens automatically when block completes
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
231
251
|
// CRITICAL FIX: Check if writer exists before trying to finish it
|
|
232
252
|
if (self.writer) {
|
|
233
253
|
// Only mark as finished if writerInput exists
|
|
@@ -243,16 +263,16 @@ static dispatch_queue_t g_audioCaptureQueue = nil;
|
|
|
243
263
|
dispatch_semaphore_signal(semaphore);
|
|
244
264
|
}];
|
|
245
265
|
|
|
246
|
-
// Reduced timeout to
|
|
247
|
-
dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(
|
|
248
|
-
dispatch_semaphore_wait(semaphore, timeout);
|
|
266
|
+
// Reduced timeout to 1 second for external devices
|
|
267
|
+
dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC));
|
|
268
|
+
long result = dispatch_semaphore_wait(semaphore, timeout);
|
|
249
269
|
|
|
250
|
-
if (!finished) {
|
|
251
|
-
MRLog(@"⚠️ AudioRecorder: Timed out waiting for writer
|
|
270
|
+
if (result != 0 || !finished) {
|
|
271
|
+
MRLog(@"⚠️ AudioRecorder: Timed out waiting for writer (external device?) - forcing cancel");
|
|
252
272
|
// Force cancel if timeout
|
|
253
273
|
[self.writer cancelWriting];
|
|
254
274
|
} else {
|
|
255
|
-
MRLog(@"✅ AudioRecorder
|
|
275
|
+
MRLog(@"✅ AudioRecorder writer finished successfully");
|
|
256
276
|
}
|
|
257
277
|
} else {
|
|
258
278
|
MRLog(@"⚠️ AudioRecorder: No writer to finish (no audio captured)");
|
|
@@ -264,6 +284,7 @@ static dispatch_queue_t g_audioCaptureQueue = nil;
|
|
|
264
284
|
self.startTime = kCMTimeInvalid;
|
|
265
285
|
self.outputPath = nil;
|
|
266
286
|
|
|
287
|
+
MRLog(@"✅ AudioRecorder stopped (safe for external devices)");
|
|
267
288
|
return YES;
|
|
268
289
|
}
|
|
269
290
|
|
|
@@ -334,11 +355,37 @@ NSArray<NSDictionary *> *listAudioCaptureDevices() {
|
|
|
334
355
|
position:AVCaptureDevicePositionUnspecified];
|
|
335
356
|
|
|
336
357
|
for (AVCaptureDevice *device in session.devices) {
|
|
358
|
+
// PRIORITY FIX: MacBook built-in devices should be default, not external devices
|
|
359
|
+
// Check if this is a built-in device (MacBook's own microphone)
|
|
360
|
+
NSString *deviceName = device.localizedName ?: @"";
|
|
361
|
+
BOOL isBuiltIn = NO;
|
|
362
|
+
|
|
363
|
+
// Built-in detection: Check for "MacBook", "iMac", "Mac Studio", "Mac mini", "Mac Pro" in name
|
|
364
|
+
if ([deviceName rangeOfString:@"MacBook" options:NSCaseInsensitiveSearch].location != NSNotFound ||
|
|
365
|
+
[deviceName rangeOfString:@"iMac" options:NSCaseInsensitiveSearch].location != NSNotFound ||
|
|
366
|
+
[deviceName rangeOfString:@"Mac Studio" options:NSCaseInsensitiveSearch].location != NSNotFound ||
|
|
367
|
+
[deviceName rangeOfString:@"Mac mini" options:NSCaseInsensitiveSearch].location != NSNotFound ||
|
|
368
|
+
[deviceName rangeOfString:@"Mac Pro" options:NSCaseInsensitiveSearch].location != NSNotFound) {
|
|
369
|
+
isBuiltIn = YES;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Also check for generic "Built-in" in name
|
|
373
|
+
if ([deviceName rangeOfString:@"Built-in" options:NSCaseInsensitiveSearch].location != NSNotFound) {
|
|
374
|
+
isBuiltIn = YES;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// External devices (Continuity, USB, etc.) should NOT be default
|
|
378
|
+
if ([deviceName rangeOfString:@"Continuity" options:NSCaseInsensitiveSearch].location != NSNotFound ||
|
|
379
|
+
[deviceName rangeOfString:@"iPhone" options:NSCaseInsensitiveSearch].location != NSNotFound ||
|
|
380
|
+
[deviceName rangeOfString:@"iPad" options:NSCaseInsensitiveSearch].location != NSNotFound) {
|
|
381
|
+
isBuiltIn = NO;
|
|
382
|
+
}
|
|
383
|
+
|
|
337
384
|
NSDictionary *info = @{
|
|
338
385
|
@"id": device.uniqueID ?: @"",
|
|
339
|
-
@"name":
|
|
386
|
+
@"name": deviceName,
|
|
340
387
|
@"manufacturer": device.manufacturer ?: @"",
|
|
341
|
-
@"isDefault": @(
|
|
388
|
+
@"isDefault": @(isBuiltIn), // Only built-in devices are default
|
|
342
389
|
@"transportType": @(device.transportType)
|
|
343
390
|
};
|
|
344
391
|
[devicesInfo addObject:info];
|
|
@@ -120,16 +120,30 @@ extern "C" bool startAVFoundationRecording(const std::string& outputPath,
|
|
|
120
120
|
codecKey = AVVideoCodecH264;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
// QUALITY FIX: ULTRA HIGH quality screen recording
|
|
124
|
+
// ProMotion displays may capture at 10 FPS - use very high bitrate for perfect quality
|
|
125
|
+
NSInteger bitrate = (NSInteger)(recordingSize.width * recordingSize.height * 30);
|
|
126
|
+
bitrate = MAX(bitrate, 30 * 1000 * 1000); // Minimum 30 Mbps
|
|
127
|
+
bitrate = MIN(bitrate, 120 * 1000 * 1000); // Maximum 120 Mbps
|
|
128
|
+
|
|
129
|
+
NSLog(@"🎬 ULTRA QUALITY AVFoundation: %dx%d, bitrate=%.2fMbps",
|
|
130
|
+
(int)recordingSize.width, (int)recordingSize.height, bitrate / (1000.0 * 1000.0));
|
|
131
|
+
|
|
123
132
|
NSDictionary *videoSettings = @{
|
|
124
133
|
AVVideoCodecKey: codecKey,
|
|
125
134
|
AVVideoWidthKey: @((int)recordingSize.width),
|
|
126
135
|
AVVideoHeightKey: @((int)recordingSize.height),
|
|
127
136
|
AVVideoCompressionPropertiesKey: @{
|
|
128
|
-
AVVideoAverageBitRateKey: @(
|
|
129
|
-
AVVideoMaxKeyFrameIntervalKey: @30
|
|
137
|
+
AVVideoAverageBitRateKey: @(bitrate),
|
|
138
|
+
AVVideoMaxKeyFrameIntervalKey: @30,
|
|
139
|
+
AVVideoAllowFrameReorderingKey: @YES,
|
|
140
|
+
AVVideoExpectedSourceFrameRateKey: @60,
|
|
141
|
+
AVVideoQualityKey: @(0.95), // 0.0-1.0, higher is better
|
|
142
|
+
AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel,
|
|
143
|
+
AVVideoH264EntropyModeKey: AVVideoH264EntropyModeCABAC
|
|
130
144
|
}
|
|
131
145
|
};
|
|
132
|
-
|
|
146
|
+
|
|
133
147
|
NSLog(@"🔧 Using codec: %@", codecKey);
|
|
134
148
|
|
|
135
149
|
// Create video input
|
package/src/camera_recorder.mm
CHANGED
|
@@ -189,18 +189,58 @@ static BOOL MRIsContinuityCamera(AVCaptureDevice *device) {
|
|
|
189
189
|
position = @"unspecified";
|
|
190
190
|
break;
|
|
191
191
|
}
|
|
192
|
-
|
|
192
|
+
|
|
193
|
+
// PRIORITY FIX: MacBook built-in cameras should be default, not external cameras
|
|
194
|
+
// Check if this is a built-in camera (MacBook's own camera)
|
|
195
|
+
NSString *deviceName = device.localizedName ?: @"";
|
|
196
|
+
NSString *deviceType = device.deviceType ?: @"";
|
|
197
|
+
BOOL isBuiltIn = NO;
|
|
198
|
+
|
|
199
|
+
// Built-in detection: Check for common built-in camera names
|
|
200
|
+
if ([deviceName rangeOfString:@"FaceTime" options:NSCaseInsensitiveSearch].location != NSNotFound ||
|
|
201
|
+
[deviceName rangeOfString:@"iSight" options:NSCaseInsensitiveSearch].location != NSNotFound ||
|
|
202
|
+
[deviceName rangeOfString:@"Built-in" options:NSCaseInsensitiveSearch].location != NSNotFound) {
|
|
203
|
+
isBuiltIn = YES;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Check device type for built-in wide angle camera
|
|
207
|
+
if (@available(macOS 10.15, *)) {
|
|
208
|
+
if ([deviceType isEqualToString:AVCaptureDeviceTypeBuiltInWideAngleCamera]) {
|
|
209
|
+
isBuiltIn = YES;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// External devices (Continuity Camera, iPhone, iPad, USB) should NOT be default
|
|
214
|
+
if (continuityCamera ||
|
|
215
|
+
[deviceName rangeOfString:@"iPhone" options:NSCaseInsensitiveSearch].location != NSNotFound ||
|
|
216
|
+
[deviceName rangeOfString:@"iPad" options:NSCaseInsensitiveSearch].location != NSNotFound ||
|
|
217
|
+
[deviceName rangeOfString:@"Continuity" options:NSCaseInsensitiveSearch].location != NSNotFound) {
|
|
218
|
+
isBuiltIn = NO;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// External device types should not be default
|
|
222
|
+
if (@available(macOS 14.0, *)) {
|
|
223
|
+
if ([deviceType isEqualToString:AVCaptureDeviceTypeExternal] ||
|
|
224
|
+
[deviceType isEqualToString:AVCaptureDeviceTypeContinuityCamera]) {
|
|
225
|
+
isBuiltIn = NO;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if ([deviceType isEqualToString:AVCaptureDeviceTypeExternalUnknown]) {
|
|
229
|
+
isBuiltIn = NO;
|
|
230
|
+
}
|
|
231
|
+
|
|
193
232
|
NSDictionary *deviceInfo = @{
|
|
194
233
|
@"id": device.uniqueID ?: @"",
|
|
195
|
-
@"name":
|
|
234
|
+
@"name": deviceName,
|
|
196
235
|
@"model": device.modelID ?: @"",
|
|
197
236
|
@"manufacturer": device.manufacturer ?: @"",
|
|
198
237
|
@"position": position ?: @"unspecified",
|
|
199
238
|
@"transportType": @(device.transportType),
|
|
200
239
|
@"isConnected": @(device.isConnected),
|
|
240
|
+
@"isDefault": @(isBuiltIn), // Only built-in cameras are default
|
|
201
241
|
@"hasFlash": @(device.hasFlash),
|
|
202
242
|
@"supportsDepth": @NO,
|
|
203
|
-
@"deviceType":
|
|
243
|
+
@"deviceType": deviceType,
|
|
204
244
|
@"requiresContinuityCameraPermission": @(continuityCamera),
|
|
205
245
|
@"maxResolution": @{
|
|
206
246
|
@"width": @(bestDimensions.width),
|
|
@@ -678,17 +718,33 @@ static BOOL MRIsContinuityCamera(AVCaptureDevice *device) {
|
|
|
678
718
|
return YES;
|
|
679
719
|
}
|
|
680
720
|
|
|
721
|
+
// CRITICAL FIX: For external cameras (especially Continuity Camera/iPhone),
|
|
722
|
+
// stopRunning can hang if device is disconnected. Use async approach.
|
|
723
|
+
MRLog(@"🛑 CameraRecorder: Stopping session (external device safe)...");
|
|
724
|
+
|
|
681
725
|
self.isShuttingDown = YES;
|
|
682
726
|
self.isRecording = NO;
|
|
683
727
|
|
|
684
|
-
|
|
685
|
-
[self.session stopRunning];
|
|
686
|
-
} @catch (NSException *exception) {
|
|
687
|
-
MRLog(@"⚠️ CameraRecorder: Exception while stopping session: %@", exception.reason);
|
|
688
|
-
}
|
|
689
|
-
|
|
728
|
+
// Stop delegate FIRST to prevent new frames
|
|
690
729
|
[self.videoOutput setSampleBufferDelegate:nil queue:nil];
|
|
691
730
|
|
|
731
|
+
// Stop session on background thread to avoid blocking
|
|
732
|
+
AVCaptureSession *sessionToStop = self.session;
|
|
733
|
+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
|
|
734
|
+
@autoreleasepool {
|
|
735
|
+
@try {
|
|
736
|
+
if ([sessionToStop isRunning]) {
|
|
737
|
+
MRLog(@"🛑 Stopping AVCaptureSession (camera)...");
|
|
738
|
+
[sessionToStop stopRunning];
|
|
739
|
+
MRLog(@"✅ AVCaptureSession stopped (camera)");
|
|
740
|
+
}
|
|
741
|
+
} @catch (NSException *exception) {
|
|
742
|
+
MRLog(@"⚠️ CameraRecorder: Exception while stopping session: %@", exception.reason);
|
|
743
|
+
}
|
|
744
|
+
// Release happens automatically when block completes
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
|
|
692
748
|
// CRITICAL FIX: Check if assetWriter exists before trying to finish it
|
|
693
749
|
// If no frames were captured, assetWriter will be nil
|
|
694
750
|
if (!self.assetWriter) {
|
|
@@ -709,12 +765,12 @@ static BOOL MRIsContinuityCamera(AVCaptureDevice *device) {
|
|
|
709
765
|
dispatch_semaphore_signal(semaphore);
|
|
710
766
|
}];
|
|
711
767
|
|
|
712
|
-
// Reduced timeout to
|
|
713
|
-
dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(
|
|
714
|
-
dispatch_semaphore_wait(semaphore, timeout);
|
|
768
|
+
// Reduced timeout to 1 second for external devices
|
|
769
|
+
dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC));
|
|
770
|
+
long result = dispatch_semaphore_wait(semaphore, timeout);
|
|
715
771
|
|
|
716
|
-
if (!finished) {
|
|
717
|
-
MRLog(@"⚠️ CameraRecorder: Timed out waiting for writer
|
|
772
|
+
if (result != 0 || !finished) {
|
|
773
|
+
MRLog(@"⚠️ CameraRecorder: Timed out waiting for writer (external device?) - forcing cancel");
|
|
718
774
|
// Force cancel if timeout
|
|
719
775
|
[self.assetWriter cancelWriting];
|
|
720
776
|
}
|
|
@@ -723,10 +779,11 @@ static BOOL MRIsContinuityCamera(AVCaptureDevice *device) {
|
|
|
723
779
|
if (!success) {
|
|
724
780
|
MRLog(@"⚠️ CameraRecorder: Writer finished with status %ld error %@", (long)self.assetWriter.status, self.assetWriter.error);
|
|
725
781
|
} else {
|
|
726
|
-
MRLog(@"✅ CameraRecorder
|
|
782
|
+
MRLog(@"✅ CameraRecorder writer finished successfully");
|
|
727
783
|
}
|
|
728
784
|
|
|
729
785
|
[self resetState];
|
|
786
|
+
MRLog(@"✅ CameraRecorder stopped (safe for external devices)");
|
|
730
787
|
return success;
|
|
731
788
|
}
|
|
732
789
|
|
package/src/mac_recorder.mm
CHANGED
|
@@ -827,6 +827,7 @@ Napi::Value GetCameraDevices(const Napi::CallbackInfo& info) {
|
|
|
827
827
|
NSString *position = camera[@"position"];
|
|
828
828
|
NSNumber *transportType = camera[@"transportType"];
|
|
829
829
|
NSNumber *isConnected = camera[@"isConnected"];
|
|
830
|
+
NSNumber *isDefault = camera[@"isDefault"];
|
|
830
831
|
NSNumber *hasFlash = camera[@"hasFlash"];
|
|
831
832
|
NSNumber *supportsDepth = camera[@"supportsDepth"];
|
|
832
833
|
|
|
@@ -861,7 +862,11 @@ Napi::Value GetCameraDevices(const Napi::CallbackInfo& info) {
|
|
|
861
862
|
if (isConnected && [isConnected isKindOfClass:[NSNumber class]]) {
|
|
862
863
|
cameraObj.Set("isConnected", Napi::Boolean::New(env, [isConnected boolValue]));
|
|
863
864
|
}
|
|
864
|
-
|
|
865
|
+
|
|
866
|
+
if (isDefault && [isDefault isKindOfClass:[NSNumber class]]) {
|
|
867
|
+
cameraObj.Set("isDefault", Napi::Boolean::New(env, [isDefault boolValue]));
|
|
868
|
+
}
|
|
869
|
+
|
|
865
870
|
if (hasFlash && [hasFlash isKindOfClass:[NSNumber class]]) {
|
|
866
871
|
cameraObj.Set("hasFlash", Napi::Boolean::New(env, [hasFlash boolValue]));
|
|
867
872
|
}
|
|
@@ -33,6 +33,10 @@ static BOOL g_audioWriterStarted = NO;
|
|
|
33
33
|
static NSInteger g_configuredSampleRate = 48000;
|
|
34
34
|
static NSInteger g_configuredChannelCount = 2;
|
|
35
35
|
|
|
36
|
+
// Frame rate debugging
|
|
37
|
+
static NSInteger g_frameCount = 0;
|
|
38
|
+
static CFAbsoluteTime g_firstFrameTime = 0;
|
|
39
|
+
|
|
36
40
|
static void CleanupWriters(void);
|
|
37
41
|
static AVAssetWriterInputPixelBufferAdaptor * _Nullable CurrentPixelBufferAdaptor(void) {
|
|
38
42
|
if (!g_pixelBufferAdaptorRef) {
|
|
@@ -90,6 +94,10 @@ static void CleanupWriters(void) {
|
|
|
90
94
|
}
|
|
91
95
|
g_videoWriterStarted = NO;
|
|
92
96
|
g_videoStartTime = kCMTimeInvalid;
|
|
97
|
+
|
|
98
|
+
// Reset frame counting
|
|
99
|
+
g_frameCount = 0;
|
|
100
|
+
g_firstFrameTime = 0;
|
|
93
101
|
}
|
|
94
102
|
|
|
95
103
|
if (g_audioWriter) {
|
|
@@ -217,6 +225,17 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
|
|
|
217
225
|
if (!appended) {
|
|
218
226
|
NSLog(@"⚠️ Failed appending pixel buffer: %@", g_videoWriter.error);
|
|
219
227
|
}
|
|
228
|
+
|
|
229
|
+
// Frame rate debugging
|
|
230
|
+
g_frameCount++;
|
|
231
|
+
if (g_firstFrameTime == 0) {
|
|
232
|
+
g_firstFrameTime = CFAbsoluteTimeGetCurrent();
|
|
233
|
+
}
|
|
234
|
+
if (g_frameCount % 60 == 0) {
|
|
235
|
+
CFAbsoluteTime elapsed = CFAbsoluteTimeGetCurrent() - g_firstFrameTime;
|
|
236
|
+
double actualFPS = g_frameCount / elapsed;
|
|
237
|
+
MRLog(@"📊 Frame stats: %ld frames in %.1fs = %.1f FPS", (long)g_frameCount, elapsed, actualFPS);
|
|
238
|
+
}
|
|
220
239
|
}
|
|
221
240
|
@end
|
|
222
241
|
|
|
@@ -310,11 +329,27 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
|
|
|
310
329
|
return NO;
|
|
311
330
|
}
|
|
312
331
|
|
|
332
|
+
// QUALITY FIX: ULTRA HIGH quality for screen recording
|
|
333
|
+
// ProMotion displays may run at 10Hz (low power) = 10 FPS capture
|
|
334
|
+
// Solution: Use VERY HIGH bitrate so each frame is perfect quality
|
|
335
|
+
// Use 30x multiplier for ULTRA quality (was 6x - way too low!)
|
|
336
|
+
NSInteger bitrate = (NSInteger)(width * height * 30);
|
|
337
|
+
bitrate = MAX(bitrate, 30 * 1000 * 1000); // Minimum 30 Mbps for crystal clear screen recording
|
|
338
|
+
bitrate = MIN(bitrate, 120 * 1000 * 1000); // Maximum 120 Mbps for ultra quality
|
|
339
|
+
|
|
340
|
+
MRLog(@"🎬 ULTRA QUALITY Screen encoder: %ldx%ld, bitrate=%.2fMbps",
|
|
341
|
+
(long)width, (long)height, bitrate / (1000.0 * 1000.0));
|
|
342
|
+
|
|
313
343
|
NSDictionary *compressionProps = @{
|
|
314
|
-
AVVideoAverageBitRateKey: @(
|
|
315
|
-
AVVideoMaxKeyFrameIntervalKey: @30
|
|
344
|
+
AVVideoAverageBitRateKey: @(bitrate),
|
|
345
|
+
AVVideoMaxKeyFrameIntervalKey: @30,
|
|
346
|
+
AVVideoAllowFrameReorderingKey: @YES,
|
|
347
|
+
AVVideoExpectedSourceFrameRateKey: @60,
|
|
348
|
+
AVVideoQualityKey: @(0.95), // 0.0-1.0, higher is better (0.95 = excellent)
|
|
349
|
+
AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel,
|
|
350
|
+
AVVideoH264EntropyModeKey: AVVideoH264EntropyModeCABAC
|
|
316
351
|
};
|
|
317
|
-
|
|
352
|
+
|
|
318
353
|
NSDictionary *videoSettings = @{
|
|
319
354
|
AVVideoCodecKey: AVVideoCodecTypeH264,
|
|
320
355
|
AVVideoWidthKey: @(width),
|
|
@@ -602,13 +637,20 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
|
|
|
602
637
|
}
|
|
603
638
|
}
|
|
604
639
|
|
|
605
|
-
// Configure stream with
|
|
640
|
+
// Configure stream with HIGH QUALITY settings
|
|
606
641
|
SCStreamConfiguration *streamConfig = [[SCStreamConfiguration alloc] init];
|
|
607
642
|
streamConfig.width = recordingWidth;
|
|
608
643
|
streamConfig.height = recordingHeight;
|
|
609
|
-
streamConfig.minimumFrameInterval = CMTimeMake(1,
|
|
644
|
+
streamConfig.minimumFrameInterval = CMTimeMake(1, 60); // 60 FPS for smooth recording
|
|
610
645
|
streamConfig.pixelFormat = kCVPixelFormatType_32BGRA;
|
|
611
646
|
streamConfig.scalesToFit = NO;
|
|
647
|
+
|
|
648
|
+
// QUALITY FIX: Set high quality encoding parameters
|
|
649
|
+
if (@available(macOS 13.0, *)) {
|
|
650
|
+
streamConfig.queueDepth = 8; // Larger queue for smoother capture
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
MRLog(@"🎬 ScreenCaptureKit config: %ldx%ld @ 60fps", (long)recordingWidth, (long)recordingHeight);
|
|
612
654
|
|
|
613
655
|
BOOL shouldCaptureMic = includeMicrophone ? [includeMicrophone boolValue] : NO;
|
|
614
656
|
BOOL shouldCaptureSystemAudio = includeSystemAudio ? [includeSystemAudio boolValue] : NO;
|