node-mac-recorder 2.21.39 → 2.21.41
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 +26 -1
- package/CREAVIT_CODE_SNIPPETS.md +832 -0
- package/CREAVIT_INTEGRATION.md +590 -0
- package/CURSOR_MAPPING.md +112 -0
- package/DUAL_RECORDING_PLAN.md +243 -0
- package/MULTI_RECORDING.md +270 -0
- package/MultiWindowRecorder.js +546 -0
- package/README.md +51 -0
- package/binding.gyp +1 -0
- package/index-multiprocess.js +238 -0
- package/index.js +174 -19
- package/package.json +1 -1
- package/recorder-worker.js +399 -0
- package/src/audio_mixer.mm +269 -0
- package/src/audio_recorder.mm +9 -0
- package/src/camera_recorder.mm +452 -260
- package/src/cursor_tracker.mm +75 -60
- package/src/mac_recorder.mm +279 -68
- package/src/screen_capture_kit.h +18 -5
- package/src/screen_capture_kit.mm +968 -387
package/src/camera_recorder.mm
CHANGED
|
@@ -5,6 +5,18 @@
|
|
|
5
5
|
#import "logging.h"
|
|
6
6
|
#import "sync_timeline.h"
|
|
7
7
|
|
|
8
|
+
#ifdef __cplusplus
|
|
9
|
+
extern "C" {
|
|
10
|
+
#endif
|
|
11
|
+
double MRActiveStopLimitSeconds(void);
|
|
12
|
+
double MRScreenRecordingStartTimestampSeconds(void);
|
|
13
|
+
double currentCameraRecordingStartTime(void);
|
|
14
|
+
#ifdef __cplusplus
|
|
15
|
+
}
|
|
16
|
+
#endif
|
|
17
|
+
|
|
18
|
+
static double g_cameraStartTimestamp = 0.0;
|
|
19
|
+
|
|
8
20
|
#ifndef AVVideoCodecTypeVP9
|
|
9
21
|
static AVVideoCodecType const AVVideoCodecTypeVP9 = @"vp09";
|
|
10
22
|
#endif
|
|
@@ -71,26 +83,123 @@ static BOOL MRIsContinuityCamera(AVCaptureDevice *device) {
|
|
|
71
83
|
return NO;
|
|
72
84
|
}
|
|
73
85
|
|
|
86
|
+
static void MRTrimMovieFileIfNeeded(NSString *path, double stopLimitSeconds, double headTrimSeconds) {
|
|
87
|
+
BOOL hasStopLimit = stopLimitSeconds > 0.0;
|
|
88
|
+
BOOL hasHeadTrim = headTrimSeconds > 0.0;
|
|
89
|
+
if (!path || [path length] == 0 || (!hasStopLimit && !hasHeadTrim)) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
NSURL *url = [NSURL fileURLWithPath:path];
|
|
94
|
+
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:nil];
|
|
95
|
+
if (!asset) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
CMTime duration = asset.duration;
|
|
100
|
+
if (!CMTIME_IS_NUMERIC(duration) || duration.value == 0) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
double assetSeconds = CMTimeGetSeconds(duration);
|
|
105
|
+
double tolerance = 0.03;
|
|
106
|
+
if (!hasStopLimit) {
|
|
107
|
+
stopLimitSeconds = assetSeconds;
|
|
108
|
+
}
|
|
109
|
+
if (assetSeconds <= stopLimitSeconds + tolerance && !hasHeadTrim) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
int32_t timescale = duration.timescale > 0 ? duration.timescale : 600;
|
|
114
|
+
double startTrimSeconds = hasHeadTrim ? MIN(headTrimSeconds, stopLimitSeconds - tolerance) : 0.0;
|
|
115
|
+
if (startTrimSeconds < 0.0) {
|
|
116
|
+
startTrimSeconds = 0.0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
CMTime targetDuration = CMTimeMakeWithSeconds(stopLimitSeconds, timescale);
|
|
120
|
+
if (CMTIME_COMPARE_INLINE(targetDuration, <=, kCMTimeZero)) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
CMTime startTime = CMTimeMakeWithSeconds(startTrimSeconds, timescale);
|
|
125
|
+
if (CMTIME_COMPARE_INLINE(startTime, >=, targetDuration)) {
|
|
126
|
+
startTime = kCMTimeZero;
|
|
127
|
+
}
|
|
128
|
+
CMTime effectiveDuration = CMTimeSubtract(targetDuration, startTime);
|
|
129
|
+
if (CMTIME_COMPARE_INLINE(effectiveDuration, <=, kCMTimeZero)) {
|
|
130
|
+
startTime = kCMTimeZero;
|
|
131
|
+
effectiveDuration = targetDuration;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
CMTimeRange trimRange = CMTimeRangeMake(startTime, effectiveDuration);
|
|
135
|
+
|
|
136
|
+
NSString *extension = path.pathExtension.lowercaseString;
|
|
137
|
+
NSString *tempPath = [[path stringByDeletingPathExtension]
|
|
138
|
+
stringByAppendingFormat:@"_trim.tmp.%@", extension.length > 0 ? extension : @"mov"];
|
|
139
|
+
[[NSFileManager defaultManager] removeItemAtPath:tempPath error:nil];
|
|
140
|
+
|
|
141
|
+
AVAssetExportSession *exportSession = [[AVAssetExportSession alloc] initWithAsset:asset
|
|
142
|
+
presetName:AVAssetExportPresetPassthrough];
|
|
143
|
+
if (!exportSession) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
exportSession.timeRange = trimRange;
|
|
147
|
+
exportSession.outputURL = [NSURL fileURLWithPath:tempPath];
|
|
148
|
+
|
|
149
|
+
NSString *fileType = AVFileTypeQuickTimeMovie;
|
|
150
|
+
if ([extension isEqualToString:@"mp4"]) {
|
|
151
|
+
fileType = AVFileTypeMPEG4;
|
|
152
|
+
} else if ([extension isEqualToString:@"mov"]) {
|
|
153
|
+
fileType = AVFileTypeQuickTimeMovie;
|
|
154
|
+
}
|
|
155
|
+
exportSession.outputFileType = fileType;
|
|
156
|
+
|
|
157
|
+
if (startTrimSeconds > 0.0) {
|
|
158
|
+
MRLog(@"✂️ Trimming camera head by %.3f s (target duration %.3f s) for %@", startTrimSeconds, stopLimitSeconds, path);
|
|
159
|
+
} else {
|
|
160
|
+
MRLog(@"✂️ Trimming %@ to %.3f seconds", path, stopLimitSeconds);
|
|
161
|
+
}
|
|
162
|
+
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
|
|
163
|
+
[exportSession exportAsynchronouslyWithCompletionHandler:^{
|
|
164
|
+
dispatch_semaphore_signal(semaphore);
|
|
165
|
+
}];
|
|
166
|
+
dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(30 * NSEC_PER_SEC));
|
|
167
|
+
dispatch_semaphore_wait(semaphore, timeout);
|
|
168
|
+
|
|
169
|
+
if (exportSession.status == AVAssetExportSessionStatusCompleted) {
|
|
170
|
+
NSError *removeError = nil;
|
|
171
|
+
[[NSFileManager defaultManager] removeItemAtPath:path error:&removeError];
|
|
172
|
+
if (removeError && removeError.code != NSFileNoSuchFileError) {
|
|
173
|
+
MRLog(@"⚠️ Failed removing original camera file before trim replace: %@", removeError);
|
|
174
|
+
}
|
|
175
|
+
NSError *moveError = nil;
|
|
176
|
+
if (![[NSFileManager defaultManager] moveItemAtPath:tempPath toPath:path error:&moveError]) {
|
|
177
|
+
MRLog(@"⚠️ Failed to replace camera file with trimmed version: %@", moveError);
|
|
178
|
+
} else {
|
|
179
|
+
MRLog(@"✅ Camera file trimmed to %.3f seconds", stopLimitSeconds);
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
[[NSFileManager defaultManager] removeItemAtPath:tempPath error:nil];
|
|
183
|
+
if (exportSession.error) {
|
|
184
|
+
MRLog(@"⚠️ Camera trim export failed: %@", exportSession.error);
|
|
185
|
+
} else {
|
|
186
|
+
MRLog(@"⚠️ Camera trim export did not complete (status %ld)", (long)exportSession.status);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
74
191
|
// Dedicated camera recorder used alongside screen capture
|
|
75
|
-
|
|
192
|
+
// ELECTRON FIX: Using MovieFileOutput instead of VideoDataOutput to avoid buffer conflicts
|
|
193
|
+
@interface CameraRecorder : NSObject<AVCaptureFileOutputRecordingDelegate>
|
|
76
194
|
|
|
77
195
|
@property (nonatomic, strong) AVCaptureSession *session;
|
|
78
196
|
@property (nonatomic, strong) AVCaptureDeviceInput *deviceInput;
|
|
79
|
-
@property (nonatomic, strong)
|
|
80
|
-
@property (nonatomic, strong) AVAssetWriter *assetWriter;
|
|
81
|
-
@property (nonatomic, strong) AVAssetWriterInput *assetWriterInput;
|
|
82
|
-
@property (nonatomic, strong) AVAssetWriterInputPixelBufferAdaptor *pixelBufferAdaptor;
|
|
83
|
-
@property (nonatomic, strong) dispatch_queue_t captureQueue;
|
|
197
|
+
@property (nonatomic, strong) AVCaptureMovieFileOutput *movieFileOutput;
|
|
84
198
|
@property (nonatomic, strong) NSString *outputPath;
|
|
85
199
|
@property (atomic, assign) BOOL isRecording;
|
|
86
|
-
@property (
|
|
87
|
-
@property (atomic, assign) BOOL
|
|
88
|
-
@property (
|
|
89
|
-
@property (nonatomic, assign) int32_t expectedWidth;
|
|
90
|
-
@property (nonatomic, assign) int32_t expectedHeight;
|
|
91
|
-
@property (nonatomic, assign) double expectedFrameRate;
|
|
92
|
-
@property (atomic, assign) BOOL needsReconfiguration;
|
|
93
|
-
@property (nonatomic, strong) NSMutableArray<NSValue *> *pendingSampleBuffers;
|
|
200
|
+
@property (nonatomic, strong) dispatch_semaphore_t recordingStartedSemaphore;
|
|
201
|
+
@property (atomic, assign) BOOL recordingStartCompleted;
|
|
202
|
+
@property (atomic, assign) BOOL recordingStartSucceeded;
|
|
94
203
|
|
|
95
204
|
+ (instancetype)sharedRecorder;
|
|
96
205
|
+ (NSArray<NSDictionary *> *)availableCameraDevices;
|
|
@@ -106,11 +215,48 @@ static BOOL MRIsContinuityCamera(AVCaptureDevice *device) {
|
|
|
106
215
|
- (instancetype)init {
|
|
107
216
|
self = [super init];
|
|
108
217
|
if (self) {
|
|
109
|
-
|
|
218
|
+
// MovieFileOutput-based recorder - no buffer management needed
|
|
219
|
+
_recordingStartCompleted = YES;
|
|
220
|
+
_recordingStartSucceeded = NO;
|
|
110
221
|
}
|
|
111
222
|
return self;
|
|
112
223
|
}
|
|
113
224
|
|
|
225
|
+
- (void)prepareRecordingStartSignal {
|
|
226
|
+
self.recordingStartCompleted = NO;
|
|
227
|
+
self.recordingStartSucceeded = NO;
|
|
228
|
+
self.recordingStartedSemaphore = dispatch_semaphore_create(0);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
- (void)finishRecordingStart:(BOOL)success {
|
|
232
|
+
if (self.recordingStartCompleted && self.recordingStartSucceeded == success) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
self.recordingStartCompleted = YES;
|
|
236
|
+
self.recordingStartSucceeded = success;
|
|
237
|
+
dispatch_semaphore_t semaphore = self.recordingStartedSemaphore;
|
|
238
|
+
if (semaphore) {
|
|
239
|
+
dispatch_semaphore_signal(semaphore);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
- (BOOL)waitForRecordingStartWithTimeout:(NSTimeInterval)timeout {
|
|
244
|
+
if (self.recordingStartCompleted) {
|
|
245
|
+
return self.recordingStartSucceeded;
|
|
246
|
+
}
|
|
247
|
+
dispatch_semaphore_t semaphore = self.recordingStartedSemaphore;
|
|
248
|
+
if (!semaphore) {
|
|
249
|
+
return self.recordingStartSucceeded;
|
|
250
|
+
}
|
|
251
|
+
dispatch_time_t waitTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC));
|
|
252
|
+
long result = dispatch_semaphore_wait(semaphore, waitTime);
|
|
253
|
+
if (result != 0 && !self.recordingStartCompleted) {
|
|
254
|
+
return NO;
|
|
255
|
+
}
|
|
256
|
+
return self.recordingStartSucceeded;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
|
|
114
260
|
+ (instancetype)sharedRecorder {
|
|
115
261
|
static CameraRecorder *recorder = nil;
|
|
116
262
|
static dispatch_once_t onceToken;
|
|
@@ -265,36 +411,18 @@ static BOOL MRIsContinuityCamera(AVCaptureDevice *device) {
|
|
|
265
411
|
return devicesInfo;
|
|
266
412
|
}
|
|
267
413
|
|
|
414
|
+
// ELECTRON FIX: MovieFileOutput-based recorder doesn't need buffer management
|
|
415
|
+
// These methods are kept for compatibility but do nothing
|
|
268
416
|
- (void)clearPendingSampleBuffers {
|
|
269
|
-
|
|
270
|
-
if (![container isKindOfClass:[NSArray class]]) {
|
|
271
|
-
MRLog(@"⚠️ CameraRecorder: pendingSampleBuffers corrupted (%@) — resetting", NSStringFromClass([container class]));
|
|
272
|
-
self.pendingSampleBuffers = [NSMutableArray array];
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
for (NSValue *value in (NSArray *)container) {
|
|
276
|
-
CMSampleBufferRef buffer = (CMSampleBufferRef)[value pointerValue];
|
|
277
|
-
if (buffer) {
|
|
278
|
-
CFRelease(buffer);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
[self.pendingSampleBuffers removeAllObjects];
|
|
417
|
+
// No-op: MovieFileOutput manages its own buffers
|
|
282
418
|
}
|
|
283
419
|
|
|
284
420
|
- (void)resetState {
|
|
285
|
-
self.writerStarted = NO;
|
|
286
421
|
self.isRecording = NO;
|
|
287
|
-
self.isShuttingDown = NO;
|
|
288
|
-
self.firstSampleTime = kCMTimeInvalid;
|
|
289
422
|
self.session = nil;
|
|
290
423
|
self.deviceInput = nil;
|
|
291
|
-
self.
|
|
292
|
-
self.assetWriter = nil;
|
|
293
|
-
self.assetWriterInput = nil;
|
|
294
|
-
self.pixelBufferAdaptor = nil;
|
|
424
|
+
self.movieFileOutput = nil;
|
|
295
425
|
self.outputPath = nil;
|
|
296
|
-
self.captureQueue = nil;
|
|
297
|
-
[self clearPendingSampleBuffers];
|
|
298
426
|
}
|
|
299
427
|
|
|
300
428
|
- (AVCaptureDevice *)deviceForId:(NSString *)deviceId {
|
|
@@ -332,8 +460,12 @@ static BOOL MRIsContinuityCamera(AVCaptureDevice *device) {
|
|
|
332
460
|
continue;
|
|
333
461
|
}
|
|
334
462
|
|
|
335
|
-
//
|
|
336
|
-
//
|
|
463
|
+
// ELECTRON FIX: Limit resolution to 1280x720 (720p) for balance
|
|
464
|
+
// Full HD works fine with MovieFileOutput since it doesn't conflict with ScreenCaptureKit
|
|
465
|
+
// But we still cap at 720p for reasonable file sizes and performance
|
|
466
|
+
if (dims.width > 1280 || dims.height > 720) {
|
|
467
|
+
continue; // Skip formats higher than 720p
|
|
468
|
+
}
|
|
337
469
|
|
|
338
470
|
int64_t score = (int64_t)dims.width * (int64_t)dims.height;
|
|
339
471
|
|
|
@@ -387,8 +519,10 @@ static BOOL MRIsContinuityCamera(AVCaptureDevice *device) {
|
|
|
387
519
|
device.activeFormat = format;
|
|
388
520
|
}
|
|
389
521
|
|
|
390
|
-
//
|
|
391
|
-
|
|
522
|
+
// ELECTRON FIX: Use reasonable frame rate (24 FPS) for good quality
|
|
523
|
+
// MovieFileOutput works perfectly with ScreenCaptureKit - no conflicts!
|
|
524
|
+
// 24fps provides smooth motion while keeping file size reasonable
|
|
525
|
+
double targetFrameRate = frameRate > 0 ? MIN(frameRate, 24.0) : 24.0;
|
|
392
526
|
AVFrameRateRange *bestRange = nil;
|
|
393
527
|
for (AVFrameRateRange *range in format.videoSupportedFrameRateRanges) {
|
|
394
528
|
if (!bestRange || range.maxFrameRate > bestRange.maxFrameRate) {
|
|
@@ -432,6 +566,7 @@ static BOOL MRIsContinuityCamera(AVCaptureDevice *device) {
|
|
|
432
566
|
return YES;
|
|
433
567
|
}
|
|
434
568
|
|
|
569
|
+
#if 0 // ELECTRON FIX: Old buffer management code - no longer used with MovieFileOutput
|
|
435
570
|
- (BOOL)setupWriterWithURL:(NSURL *)outputURL
|
|
436
571
|
width:(int32_t)width
|
|
437
572
|
height:(int32_t)height
|
|
@@ -569,9 +704,12 @@ static BOOL MRIsContinuityCamera(AVCaptureDevice *device) {
|
|
|
569
704
|
[self.assetWriter addInput:self.assetWriterInput];
|
|
570
705
|
self.writerStarted = NO;
|
|
571
706
|
self.firstSampleTime = kCMTimeInvalid;
|
|
572
|
-
|
|
707
|
+
|
|
573
708
|
return YES;
|
|
574
709
|
}
|
|
710
|
+
#endif // End old buffer management code (setupWriterWithURL)
|
|
711
|
+
|
|
712
|
+
// MARK: - NEW MovieFileOutput Implementation (NOT disabled!)
|
|
575
713
|
|
|
576
714
|
- (BOOL)startRecordingWithDeviceId:(NSString *)deviceId
|
|
577
715
|
outputPath:(NSString *)outputPath
|
|
@@ -596,119 +734,157 @@ static BOOL MRIsContinuityCamera(AVCaptureDevice *device) {
|
|
|
596
734
|
return NO;
|
|
597
735
|
}
|
|
598
736
|
|
|
599
|
-
//
|
|
600
|
-
__block BOOL cameraPermissionGranted = YES;
|
|
737
|
+
// CRITICAL ELECTRON FIX: Non-blocking permission check
|
|
601
738
|
AVAuthorizationStatus cameraStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
|
|
602
|
-
if (cameraStatus == AVAuthorizationStatusNotDetermined) {
|
|
603
|
-
dispatch_semaphore_t permissionSemaphore = dispatch_semaphore_create(0);
|
|
604
|
-
[AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {
|
|
605
|
-
cameraPermissionGranted = granted;
|
|
606
|
-
dispatch_semaphore_signal(permissionSemaphore);
|
|
607
|
-
}];
|
|
608
|
-
dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC));
|
|
609
|
-
dispatch_semaphore_wait(permissionSemaphore, timeout);
|
|
610
|
-
} else if (cameraStatus != AVAuthorizationStatusAuthorized) {
|
|
611
|
-
cameraPermissionGranted = NO;
|
|
612
|
-
}
|
|
613
739
|
|
|
614
|
-
|
|
740
|
+
// Definitely denied - stop immediately
|
|
741
|
+
if (cameraStatus == AVAuthorizationStatusDenied || cameraStatus == AVAuthorizationStatusRestricted) {
|
|
615
742
|
if (error) {
|
|
616
|
-
NSDictionary *userInfo = @{ NSLocalizedDescriptionKey: @"Camera permission
|
|
743
|
+
NSDictionary *userInfo = @{ NSLocalizedDescriptionKey: @"Camera permission denied - please grant permission in System Settings" };
|
|
617
744
|
*error = [NSError errorWithDomain:@"CameraRecorder" code:-4 userInfo:userInfo];
|
|
618
745
|
}
|
|
619
746
|
return NO;
|
|
620
747
|
}
|
|
621
748
|
|
|
622
|
-
//
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
};
|
|
636
|
-
*error = [NSError errorWithDomain:@"CameraRecorder" code:-3 userInfo:userInfo];
|
|
637
|
-
}
|
|
638
|
-
return NO;
|
|
749
|
+
// CRITICAL ELECTRON FIX: For NotDetermined, request async and assume it will be granted
|
|
750
|
+
// Blocking/polling here causes Electron crashes
|
|
751
|
+
if (cameraStatus == AVAuthorizationStatusNotDetermined) {
|
|
752
|
+
MRLog(@"🔐 Camera permission not determined - requesting async (non-blocking)...");
|
|
753
|
+
[AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {
|
|
754
|
+
if (granted) {
|
|
755
|
+
MRLog(@"✅ Camera permission granted (async callback)");
|
|
756
|
+
} else {
|
|
757
|
+
MRLog(@"❌ Camera permission denied (async callback)");
|
|
758
|
+
}
|
|
759
|
+
}];
|
|
760
|
+
// Don't wait - camera will start when permission is granted
|
|
761
|
+
MRLog(@"📤 Permission request sent, continuing without blocking...");
|
|
639
762
|
}
|
|
640
763
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
764
|
+
// CRITICAL ELECTRON FIX: Do ALL potentially blocking operations on background thread
|
|
765
|
+
// File I/O and device locking MUST NOT block main thread
|
|
766
|
+
|
|
767
|
+
// Store output path for later use
|
|
768
|
+
self.outputPath = outputPath;
|
|
769
|
+
self.isRecording = YES; // Mark as recording early
|
|
770
|
+
[self prepareRecordingStartSignal];
|
|
771
|
+
|
|
772
|
+
// Schedule all blocking operations on background thread
|
|
773
|
+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
774
|
+
@autoreleasepool {
|
|
775
|
+
// Remove any stale file (BLOCKING I/O - safe on background thread)
|
|
776
|
+
NSError *removeError = nil;
|
|
777
|
+
[[NSFileManager defaultManager] removeItemAtPath:outputPath error:&removeError];
|
|
778
|
+
if (removeError && removeError.code != NSFileNoSuchFileError) {
|
|
779
|
+
MRLog(@"⚠️ CameraRecorder: Failed to remove existing camera file: %@", removeError);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
[self clearPendingSampleBuffers];
|
|
783
|
+
AVCaptureDevice *device = [self deviceForId:deviceId];
|
|
784
|
+
if (!device) {
|
|
785
|
+
MRLog(@"❌ No camera devices available");
|
|
786
|
+
self.isRecording = NO;
|
|
787
|
+
[self finishRecordingStart:NO];
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
if (MRIsContinuityCamera(device) && !MRAllowContinuityCamera()) {
|
|
792
|
+
MRLog(@"⚠️ Continuity Camera access denied - missing Info.plist entitlement");
|
|
793
|
+
self.isRecording = NO;
|
|
794
|
+
[self finishRecordingStart:NO];
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
int32_t width = 0;
|
|
799
|
+
int32_t height = 0;
|
|
800
|
+
double frameRate = 0.0;
|
|
801
|
+
AVCaptureDeviceFormat *bestFormat = [self bestFormatForDevice:device widthOut:&width heightOut:&height frameRateOut:&frameRate];
|
|
802
|
+
|
|
803
|
+
NSError *configError = nil;
|
|
804
|
+
if (![self configureDevice:device withFormat:bestFormat frameRate:frameRate error:&configError]) {
|
|
805
|
+
MRLog(@"❌ Failed to configure device: %@", configError);
|
|
806
|
+
self.isRecording = NO;
|
|
807
|
+
[self finishRecordingStart:NO];
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// Continue with session setup on background thread
|
|
812
|
+
[self continueSetupWithDevice:device width:width height:height frameRate:frameRate outputPath:outputPath];
|
|
647
813
|
}
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
// Return immediately - setup continues async
|
|
817
|
+
MRLog(@"📤 Camera setup scheduled on background thread (non-blocking)");
|
|
818
|
+
return YES;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
- (void)continueSetupWithDevice:(AVCaptureDevice *)device
|
|
822
|
+
width:(int32_t)width
|
|
823
|
+
height:(int32_t)height
|
|
824
|
+
frameRate:(double)frameRate
|
|
825
|
+
outputPath:(NSString *)outputPath {
|
|
826
|
+
|
|
661
827
|
self.session = [[AVCaptureSession alloc] init];
|
|
662
|
-
|
|
663
|
-
|
|
828
|
+
|
|
829
|
+
// ELECTRON FIX: Use HIGH preset for good quality
|
|
830
|
+
// MovieFileOutput works perfectly with ScreenCaptureKit - no more crashes!
|
|
831
|
+
// We already selected best format (up to 720p), so HIGH preset will use it
|
|
832
|
+
self.session.sessionPreset = AVCaptureSessionPresetHigh;
|
|
833
|
+
MRLog(@"📹 Camera session preset: HIGH (up to 720p based on selected format)");
|
|
834
|
+
|
|
835
|
+
// CRITICAL ELECTRON FIX: Use beginConfiguration / commitConfiguration
|
|
836
|
+
// This makes session configuration ATOMIC and prevents crashes
|
|
837
|
+
[self.session beginConfiguration];
|
|
838
|
+
|
|
839
|
+
NSError *localError = nil;
|
|
840
|
+
self.deviceInput = [AVCaptureDeviceInput deviceInputWithDevice:device error:&localError];
|
|
664
841
|
if (!self.deviceInput) {
|
|
842
|
+
MRLog(@"❌ Failed to create device input: %@", localError);
|
|
843
|
+
[self.session commitConfiguration];
|
|
665
844
|
[self resetState];
|
|
666
|
-
|
|
845
|
+
[self finishRecordingStart:NO];
|
|
846
|
+
return;
|
|
667
847
|
}
|
|
668
|
-
|
|
848
|
+
|
|
669
849
|
if ([self.session canAddInput:self.deviceInput]) {
|
|
670
850
|
[self.session addInput:self.deviceInput];
|
|
671
851
|
} else {
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
NSLocalizedDescriptionKey: @"Unable to add camera input to capture session"
|
|
675
|
-
};
|
|
676
|
-
*error = [NSError errorWithDomain:@"CameraRecorder" code:-6 userInfo:userInfo];
|
|
677
|
-
}
|
|
852
|
+
MRLog(@"❌ Unable to add camera input to capture session");
|
|
853
|
+
[self.session commitConfiguration];
|
|
678
854
|
[self resetState];
|
|
679
|
-
|
|
855
|
+
[self finishRecordingStart:NO];
|
|
856
|
+
return;
|
|
680
857
|
}
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
//
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
self.captureQueue = dispatch_queue_create("node_mac_recorder.camera.queue", DISPATCH_QUEUE_SERIAL);
|
|
691
|
-
[self.videoOutput setSampleBufferDelegate:self queue:self.captureQueue];
|
|
692
|
-
|
|
693
|
-
if ([self.session canAddOutput:self.videoOutput]) {
|
|
694
|
-
[self.session addOutput:self.videoOutput];
|
|
858
|
+
|
|
859
|
+
// CRITICAL ELECTRON FIX: Use AVCaptureMovieFileOutput instead of VideoDataOutput
|
|
860
|
+
// VideoDataOutput uses frame-by-frame buffers that conflict with ScreenCaptureKit
|
|
861
|
+
// MovieFileOutput writes directly to file with different buffer management
|
|
862
|
+
self.movieFileOutput = [[AVCaptureMovieFileOutput alloc] init];
|
|
863
|
+
|
|
864
|
+
if ([self.session canAddOutput:self.movieFileOutput]) {
|
|
865
|
+
[self.session addOutput:self.movieFileOutput];
|
|
866
|
+
MRLog(@"✅ Added AVCaptureMovieFileOutput (file-based recording, no buffer conflicts)");
|
|
695
867
|
} else {
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
NSLocalizedDescriptionKey: @"Unable to add camera output to capture session"
|
|
699
|
-
};
|
|
700
|
-
*error = [NSError errorWithDomain:@"CameraRecorder" code:-7 userInfo:userInfo];
|
|
701
|
-
}
|
|
868
|
+
MRLog(@"❌ Unable to add movie file output to capture session");
|
|
869
|
+
[self.session commitConfiguration];
|
|
702
870
|
[self resetState];
|
|
703
|
-
|
|
871
|
+
[self finishRecordingStart:NO];
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// CRITICAL FIX: Disable audio recording - we only want video from camera
|
|
876
|
+
// MovieFileOutput by default tries to record both audio and video
|
|
877
|
+
// Since we don't have an audio input, it won't start recording at all
|
|
878
|
+
AVCaptureConnection *audioConnection = [self.movieFileOutput connectionWithMediaType:AVMediaTypeAudio];
|
|
879
|
+
if (audioConnection) {
|
|
880
|
+
audioConnection.enabled = NO;
|
|
881
|
+
MRLog(@"🔇 Disabled audio connection (video-only recording)");
|
|
882
|
+
} else {
|
|
883
|
+
MRLog(@"ℹ️ No audio connection found (expected - video-only setup)");
|
|
704
884
|
}
|
|
705
|
-
|
|
706
|
-
AVCaptureConnection *connection = [self.videoOutput connectionWithMediaType:AVMediaTypeVideo];
|
|
707
|
-
if (connection) {
|
|
708
|
-
// DON'T set orientation - let the camera use its natural orientation
|
|
709
|
-
// The device knows best (portrait for phones, landscape for webcams)
|
|
710
|
-
// We just capture whatever comes through
|
|
711
885
|
|
|
886
|
+
AVCaptureConnection *connection = [self.movieFileOutput connectionWithMediaType:AVMediaTypeVideo];
|
|
887
|
+
if (connection) {
|
|
712
888
|
// Mirror front cameras for natural preview
|
|
713
889
|
if (connection.isVideoMirroringSupported && device.position == AVCaptureDevicePositionFront) {
|
|
714
890
|
if ([connection respondsToSelector:@selector(setAutomaticallyAdjustsVideoMirroring:)]) {
|
|
@@ -716,148 +892,159 @@ static BOOL MRIsContinuityCamera(AVCaptureDevice *device) {
|
|
|
716
892
|
}
|
|
717
893
|
connection.videoMirrored = YES;
|
|
718
894
|
}
|
|
719
|
-
|
|
720
|
-
// Log actual connection properties for debugging
|
|
721
895
|
MRLog(@"📐 Camera connection: orientation=%ld (native), mirrored=%d, format=%dx%d",
|
|
722
896
|
(long)connection.videoOrientation,
|
|
723
897
|
connection.isVideoMirrored,
|
|
724
898
|
width, height);
|
|
725
899
|
}
|
|
726
|
-
|
|
727
|
-
//
|
|
728
|
-
|
|
900
|
+
|
|
901
|
+
// CRITICAL: Commit configuration BEFORE starting
|
|
902
|
+
[self.session commitConfiguration];
|
|
903
|
+
|
|
904
|
+
// Store configuration
|
|
729
905
|
self.outputPath = outputPath;
|
|
730
906
|
self.isRecording = YES;
|
|
731
|
-
self.isShuttingDown = NO;
|
|
732
|
-
self.expectedWidth = width;
|
|
733
|
-
self.expectedHeight = height;
|
|
734
|
-
self.expectedFrameRate = frameRate;
|
|
735
|
-
self.needsReconfiguration = NO;
|
|
736
907
|
|
|
908
|
+
// CRITICAL ELECTRON FIX: Start on current thread (already on background)
|
|
909
|
+
// Don't touch main thread at all - AVCaptureSession can start on background thread
|
|
910
|
+
MRLog(@"🎥 Starting AVCaptureSession (camera) on background thread...");
|
|
737
911
|
[self.session startRunning];
|
|
738
912
|
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
return YES;
|
|
742
|
-
}
|
|
913
|
+
// Wait a moment for session to fully start
|
|
914
|
+
[NSThread sleepForTimeInterval:0.5];
|
|
743
915
|
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
916
|
+
MRLog(@"✅ AVCaptureSession started");
|
|
917
|
+
MRLog(@"🔍 Session isRunning: %d", [self.session isRunning]);
|
|
918
|
+
|
|
919
|
+
// CRITICAL FIX: MovieFileOutput only supports .mov and .mp4, NOT .webm
|
|
920
|
+
// If path ends with .webm, change it to .mov
|
|
921
|
+
NSString *finalOutputPath = outputPath;
|
|
922
|
+
if ([outputPath.pathExtension.lowercaseString isEqualToString:@"webm"]) {
|
|
923
|
+
finalOutputPath = [[outputPath stringByDeletingPathExtension] stringByAppendingPathExtension:@"mov"];
|
|
924
|
+
MRLog(@"⚠️ Camera: Changed output from .webm to .mov (MovieFileOutput doesn't support WebM)");
|
|
925
|
+
MRLog(@" New path: %@", finalOutputPath);
|
|
926
|
+
// Update stored path
|
|
927
|
+
self.outputPath = finalOutputPath;
|
|
747
928
|
}
|
|
748
929
|
|
|
749
|
-
//
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
//
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
if (parsed >= 0.0 && parsed <= 2.0) {
|
|
758
|
-
cameraTailSeconds = parsed;
|
|
759
|
-
}
|
|
930
|
+
// CRITICAL: Start file recording immediately
|
|
931
|
+
NSURL *outputURL = [NSURL fileURLWithPath:finalOutputPath];
|
|
932
|
+
|
|
933
|
+
// Verify path and output are valid
|
|
934
|
+
if (!outputURL) {
|
|
935
|
+
MRLog(@"❌ Failed to create output URL from path: %@", finalOutputPath);
|
|
936
|
+
[self finishRecordingStart:NO];
|
|
937
|
+
return;
|
|
760
938
|
}
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
// CRITICAL FIX: For external cameras (especially Continuity Camera/iPhone),
|
|
767
|
-
// stopRunning can hang if device is disconnected. Use async approach.
|
|
768
|
-
MRLog(@"🛑 CameraRecorder: Stopping session (external device safe)...");
|
|
769
|
-
|
|
770
|
-
self.isShuttingDown = YES;
|
|
771
|
-
|
|
772
|
-
// Stop session on background thread to avoid blocking, but wait briefly so we capture trailing frames.
|
|
773
|
-
AVCaptureSession *sessionToStop = self.session;
|
|
774
|
-
|
|
775
|
-
dispatch_group_t sessionStopGroup = NULL;
|
|
776
|
-
if (sessionToStop) {
|
|
777
|
-
sessionStopGroup = dispatch_group_create();
|
|
778
|
-
dispatch_group_enter(sessionStopGroup);
|
|
779
|
-
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
|
|
780
|
-
@autoreleasepool {
|
|
781
|
-
@try {
|
|
782
|
-
if ([sessionToStop isRunning]) {
|
|
783
|
-
MRLog(@"🛑 Stopping AVCaptureSession (camera)...");
|
|
784
|
-
[sessionToStop stopRunning];
|
|
785
|
-
MRLog(@"✅ AVCaptureSession stopped (camera)");
|
|
786
|
-
}
|
|
787
|
-
} @catch (NSException *exception) {
|
|
788
|
-
MRLog(@"⚠️ CameraRecorder: Exception while stopping session: %@", exception.reason);
|
|
789
|
-
}
|
|
790
|
-
dispatch_group_leave(sessionStopGroup);
|
|
791
|
-
}
|
|
792
|
-
});
|
|
939
|
+
|
|
940
|
+
if (!self.movieFileOutput) {
|
|
941
|
+
MRLog(@"❌ movieFileOutput is nil!");
|
|
942
|
+
return;
|
|
793
943
|
}
|
|
794
944
|
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
945
|
+
MRLog(@"📁 Camera output path: %@", outputPath);
|
|
946
|
+
MRLog(@"📁 Camera output URL: %@", outputURL);
|
|
947
|
+
MRLog(@"🎥 Starting movie file recording...");
|
|
948
|
+
|
|
949
|
+
[self.movieFileOutput startRecordingToOutputFileURL:outputURL recordingDelegate:self];
|
|
950
|
+
|
|
951
|
+
MRLog(@"✅ startRecordingToOutputFileURL called");
|
|
952
|
+
MRLog(@"🔍 Is recording: %d", [self.movieFileOutput isRecording]);
|
|
953
|
+
|
|
954
|
+
MRLog(@"🎥 CameraRecorder started: %@ (file-based recording)", device.localizedName);
|
|
955
|
+
MRLog(@" Format reports: %dx%d @ %.2ffps", width, height, frameRate);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// MARK: - AVCaptureFileOutputRecordingDelegate
|
|
959
|
+
|
|
960
|
+
- (void)captureOutput:(AVCaptureFileOutput *)output
|
|
961
|
+
didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL
|
|
962
|
+
fromConnections:(NSArray<AVCaptureConnection *> *)connections
|
|
963
|
+
error:(NSError *)error {
|
|
964
|
+
MRLog(@"🎬 DELEGATE: didFinishRecordingToOutputFileAtURL called");
|
|
965
|
+
MRLog(@" File URL: %@", outputFileURL.path);
|
|
966
|
+
|
|
967
|
+
if (error) {
|
|
968
|
+
MRLog(@"❌ Camera recording finished with ERROR:");
|
|
969
|
+
MRLog(@" Error code: %ld", (long)error.code);
|
|
970
|
+
MRLog(@" Error domain: %@", error.domain);
|
|
971
|
+
MRLog(@" Error description: %@", error.localizedDescription);
|
|
972
|
+
if (error.userInfo) {
|
|
973
|
+
MRLog(@" Error userInfo: %@", error.userInfo);
|
|
800
974
|
}
|
|
801
|
-
}
|
|
975
|
+
} else {
|
|
976
|
+
MRLog(@"✅ Camera recording finished SUCCESSFULLY");
|
|
977
|
+
MRLog(@" Output file: %@", outputFileURL.path);
|
|
802
978
|
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
}
|
|
807
|
-
self.isRecording = NO;
|
|
979
|
+
// Check if file actually exists
|
|
980
|
+
BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:outputFileURL.path];
|
|
981
|
+
MRLog(@" File exists on disk: %d", fileExists);
|
|
808
982
|
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
return YES; // Success - nothing to finish
|
|
983
|
+
if (fileExists) {
|
|
984
|
+
NSDictionary *attrs = [[NSFileManager defaultManager] attributesOfItemAtPath:outputFileURL.path error:nil];
|
|
985
|
+
unsigned long long fileSize = [attrs fileSize];
|
|
986
|
+
MRLog(@" File size: %llu bytes", fileSize);
|
|
987
|
+
}
|
|
815
988
|
}
|
|
816
989
|
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
990
|
+
double stopLimit = MRActiveStopLimitSeconds();
|
|
991
|
+
double screenStart = MRScreenRecordingStartTimestampSeconds();
|
|
992
|
+
double cameraStart = currentCameraRecordingStartTime();
|
|
993
|
+
double headTrim = 0.0;
|
|
994
|
+
if (screenStart > 0 && cameraStart > 0 && cameraStart < screenStart) {
|
|
995
|
+
headTrim = screenStart - cameraStart;
|
|
996
|
+
}
|
|
997
|
+
if (stopLimit > 0 || headTrim > 0) {
|
|
998
|
+
MRTrimMovieFileIfNeeded(outputFileURL.path, stopLimit, headTrim);
|
|
822
999
|
}
|
|
1000
|
+
g_cameraStartTimestamp = 0.0;
|
|
823
1001
|
|
|
824
|
-
|
|
825
|
-
|
|
1002
|
+
self.isRecording = NO;
|
|
1003
|
+
}
|
|
826
1004
|
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
1005
|
+
- (void)captureOutput:(AVCaptureFileOutput *)output
|
|
1006
|
+
didStartRecordingToOutputFileAtURL:(NSURL *)fileURL
|
|
1007
|
+
fromConnections:(NSArray<AVCaptureConnection *> *)connections {
|
|
1008
|
+
MRLog(@"🎬 DELEGATE: didStartRecordingToOutputFileAtURL called");
|
|
1009
|
+
MRLog(@" File URL: %@", fileURL.path);
|
|
1010
|
+
MRLog(@" Connections count: %lu", (unsigned long)connections.count);
|
|
1011
|
+
MRLog(@"✅ Camera file recording STARTED successfully!");
|
|
1012
|
+
[self finishRecordingStart:YES];
|
|
1013
|
+
}
|
|
831
1014
|
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
1015
|
+
- (BOOL)stopRecording {
|
|
1016
|
+
if (!self.isRecording) {
|
|
1017
|
+
return YES;
|
|
1018
|
+
}
|
|
836
1019
|
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
dispatch_time_t extendedTimeout = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(extendedWaitSeconds * NSEC_PER_SEC));
|
|
841
|
-
result = dispatch_semaphore_wait(semaphore, extendedTimeout);
|
|
1020
|
+
MRLog(@"🛑 CameraRecorder: Stopping recording...");
|
|
1021
|
+
if (!self.recordingStartCompleted) {
|
|
1022
|
+
[self finishRecordingStart:NO];
|
|
842
1023
|
}
|
|
843
1024
|
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
[
|
|
1025
|
+
// CRITICAL ELECTRON FIX: Stop movie file output (simple API)
|
|
1026
|
+
if (self.movieFileOutput && [self.movieFileOutput isRecording]) {
|
|
1027
|
+
[self.movieFileOutput stopRecording];
|
|
1028
|
+
MRLog(@"✅ Movie file output stop requested");
|
|
847
1029
|
}
|
|
848
1030
|
|
|
849
|
-
|
|
850
|
-
if (
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
MRLog(@"✅ CameraRecorder writer finished successfully");
|
|
1031
|
+
// Stop session
|
|
1032
|
+
if (self.session && [self.session isRunning]) {
|
|
1033
|
+
[self.session stopRunning];
|
|
1034
|
+
MRLog(@"✅ AVCaptureSession stopped");
|
|
854
1035
|
}
|
|
855
1036
|
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
1037
|
+
// Cleanup
|
|
1038
|
+
self.session = nil;
|
|
1039
|
+
self.deviceInput = nil;
|
|
1040
|
+
self.movieFileOutput = nil;
|
|
1041
|
+
self.outputPath = nil;
|
|
1042
|
+
|
|
1043
|
+
MRLog(@"✅ CameraRecorder stopped successfully");
|
|
1044
|
+
return YES;
|
|
859
1045
|
}
|
|
860
1046
|
|
|
1047
|
+
#if 0 // ELECTRON FIX: Old buffer management code - no longer used with MovieFileOutput
|
|
861
1048
|
- (void)enqueueSampleBuffer:(CMSampleBufferRef)sampleBuffer {
|
|
862
1049
|
if (!sampleBuffer) {
|
|
863
1050
|
return;
|
|
@@ -922,15 +1109,9 @@ static BOOL MRIsContinuityCamera(AVCaptureDevice *device) {
|
|
|
922
1109
|
if (CMTIME_IS_VALID(baseline)) {
|
|
923
1110
|
frameSeconds = CMTimeGetSeconds(CMTimeSubtract(bufferTime, baseline));
|
|
924
1111
|
}
|
|
925
|
-
//
|
|
1112
|
+
// Do NOT extend camera stop limit by audio start offset.
|
|
1113
|
+
// Clamping to the same stopLimit as audio ensures durations match.
|
|
926
1114
|
double effectiveStopLimit = stopLimit;
|
|
927
|
-
if (hasAudioStart && CMTIME_IS_VALID(baseline)) {
|
|
928
|
-
CMTime startDeltaTime = CMTimeSubtract(baseline, audioStart);
|
|
929
|
-
double startDelta = CMTimeGetSeconds(startDeltaTime);
|
|
930
|
-
if (startDelta > 0) {
|
|
931
|
-
effectiveStopLimit += startDelta;
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
1115
|
double tolerance = self.expectedFrameRate > 0 ? (1.5 / self.expectedFrameRate) : 0.02;
|
|
935
1116
|
if (tolerance < 0.02) {
|
|
936
1117
|
tolerance = 0.02;
|
|
@@ -1012,26 +1193,28 @@ static BOOL MRIsContinuityCamera(AVCaptureDevice *device) {
|
|
|
1012
1193
|
self.firstSampleTime = timestamp;
|
|
1013
1194
|
}
|
|
1014
1195
|
|
|
1015
|
-
CMTime
|
|
1016
|
-
|
|
1017
|
-
|
|
1196
|
+
CMTime baseline = kCMTimeInvalid;
|
|
1197
|
+
CMTime audioStart = MRSyncAudioFirstTimestamp();
|
|
1198
|
+
if (CMTIME_IS_VALID(audioStart)) {
|
|
1199
|
+
baseline = audioStart;
|
|
1200
|
+
} else if (CMTIME_IS_VALID(self.firstSampleTime)) {
|
|
1201
|
+
baseline = self.firstSampleTime;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
CMTime relativeTimestamp = kCMTimeZero;
|
|
1205
|
+
if (CMTIME_IS_VALID(baseline)) {
|
|
1206
|
+
relativeTimestamp = CMTimeSubtract(timestamp, baseline);
|
|
1018
1207
|
if (CMTIME_COMPARE_INLINE(relativeTimestamp, <, kCMTimeZero)) {
|
|
1019
1208
|
relativeTimestamp = kCMTimeZero;
|
|
1020
1209
|
}
|
|
1210
|
+
} else {
|
|
1211
|
+
relativeTimestamp = timestamp;
|
|
1021
1212
|
}
|
|
1022
1213
|
|
|
1023
1214
|
double stopLimit = MRSyncGetStopLimitSeconds();
|
|
1024
1215
|
if (stopLimit > 0) {
|
|
1025
|
-
//
|
|
1026
|
-
|
|
1027
|
-
if (CMTIME_IS_VALID(audioStartTS) && CMTIME_IS_VALID(self.firstSampleTime)) {
|
|
1028
|
-
CMTime startDeltaTS = CMTimeSubtract(self.firstSampleTime, audioStartTS);
|
|
1029
|
-
double startDelta = CMTimeGetSeconds(startDeltaTS);
|
|
1030
|
-
if (startDelta > 0) {
|
|
1031
|
-
stopLimit += startDelta;
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1216
|
+
// Do NOT extend camera stop limit by audio start offset.
|
|
1217
|
+
// Using the same stopLimit as audio keeps durations aligned.
|
|
1035
1218
|
double frameSeconds = CMTimeGetSeconds(relativeTimestamp);
|
|
1036
1219
|
double tolerance = self.expectedFrameRate > 0 ? (1.5 / self.expectedFrameRate) : 0.02;
|
|
1037
1220
|
if (tolerance < 0.02) {
|
|
@@ -1088,6 +1271,7 @@ static BOOL MRIsContinuityCamera(AVCaptureDevice *device) {
|
|
|
1088
1271
|
|
|
1089
1272
|
[self processSampleBufferReadyForWriting:sampleBuffer];
|
|
1090
1273
|
}
|
|
1274
|
+
#endif // End old buffer management code
|
|
1091
1275
|
|
|
1092
1276
|
@end
|
|
1093
1277
|
|
|
@@ -1105,6 +1289,14 @@ bool startCameraRecording(NSString *outputPath, NSString *deviceId, NSError **er
|
|
|
1105
1289
|
error:error];
|
|
1106
1290
|
}
|
|
1107
1291
|
|
|
1292
|
+
bool waitForCameraRecordingStart(double timeoutSeconds) {
|
|
1293
|
+
return [[CameraRecorder sharedRecorder] waitForRecordingStartWithTimeout:timeoutSeconds];
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
double currentCameraRecordingStartTime(void) {
|
|
1297
|
+
return g_cameraStartTimestamp;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1108
1300
|
bool stopCameraRecording() {
|
|
1109
1301
|
@autoreleasepool {
|
|
1110
1302
|
return [[CameraRecorder sharedRecorder] stopRecording];
|