node-mac-recorder 2.2.1 → 2.4.1

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.
@@ -1,9 +1,6 @@
1
- <<<<<<< HEAD
2
- =======
3
1
  #import "screen_capture.h"
4
2
  #import <ScreenCaptureKit/ScreenCaptureKit.h>
5
3
  #import <AVFoundation/AVFoundation.h>
6
- >>>>>>> screencapture
7
4
  #import <CoreGraphics/CoreGraphics.h>
8
5
  #import <AppKit/AppKit.h>
9
6
 
@@ -77,11 +74,7 @@
77
74
  NSURL *fileURL = [NSURL fileURLWithPath:filePath];
78
75
  CGImageDestinationRef destination = CGImageDestinationCreateWithURL(
79
76
  (__bridge CFURLRef)fileURL,
80
- <<<<<<< HEAD
81
- CFSTR("public.png"),
82
- =======
83
77
  (__bridge CFStringRef)@"public.png",
84
- >>>>>>> screencapture
85
78
  1,
86
79
  NULL
87
80
  );
@@ -110,14 +103,6 @@
110
103
 
111
104
  + (CGImageRef)createScreenshotFromDisplay:(CGDirectDisplayID)displayID
112
105
  rect:(CGRect)rect {
113
- <<<<<<< HEAD
114
- if (CGRectIsNull(rect)) {
115
- // Capture entire display
116
- return CGDisplayCreateImage(displayID);
117
- } else {
118
- // Capture specific rect
119
- return CGDisplayCreateImageForRect(displayID, rect);
120
- =======
121
106
  if (CGRectIsNull(rect) || CGRectIsEmpty(rect)) {
122
107
  rect = CGDisplayBounds(displayID);
123
108
  }
@@ -156,7 +141,6 @@ Napi::Value GetAvailableDisplays(const Napi::CallbackInfo& info) {
156
141
  displayObj.Set("isPrimary", Napi::Boolean::New(env, [[displayInfo objectForKey:@"isPrimary"] boolValue]));
157
142
 
158
143
  displaysArray.Set(static_cast<uint32_t>(i), displayObj);
159
- >>>>>>> screencapture
160
144
  }
161
145
 
162
146
  return displaysArray;
package/test-sck.js CHANGED
@@ -1,55 +1,3 @@
1
- <<<<<<< HEAD
2
- const nativeBinding = require('./build/Release/mac_recorder.node');
3
-
4
- console.log('=== ScreenCaptureKit Migration Test ===');
5
- console.log('ScreenCaptureKit available:', nativeBinding.isScreenCaptureKitAvailable());
6
- console.log('Displays:', nativeBinding.getDisplays().length);
7
- console.log('Audio devices:', nativeBinding.getAudioDevices().length);
8
- console.log('Permissions OK:', nativeBinding.checkPermissions());
9
-
10
- const displays = nativeBinding.getDisplays();
11
- console.log('\nDisplay info:', displays[0]);
12
-
13
- const audioDevices = nativeBinding.getAudioDevices();
14
- console.log('\nFirst audio device:', audioDevices[0]);
15
-
16
- // Test starting and stopping recording
17
- console.log('\n=== Recording Test ===');
18
- const outputPath = '/tmp/test-recording-sck.mov';
19
-
20
- try {
21
- console.log('Starting recording...');
22
- const success = nativeBinding.startRecording(outputPath, {
23
- displayId: displays[0].id,
24
- captureCursor: true,
25
- includeMicrophone: false,
26
- includeSystemAudio: false
27
- });
28
-
29
- console.log('Recording started:', success);
30
-
31
- if (success) {
32
- setTimeout(() => {
33
- console.log('Stopping recording...');
34
- const stopped = nativeBinding.stopRecording();
35
- console.log('Recording stopped:', stopped);
36
-
37
- // Check if file was created
38
- const fs = require('fs');
39
- setTimeout(() => {
40
- if (fs.existsSync(outputPath)) {
41
- const stats = fs.statSync(outputPath);
42
- console.log(`Recording file created: ${outputPath} (${stats.size} bytes)`);
43
- } else {
44
- console.log('Recording file not found');
45
- }
46
- }, 1000);
47
- }, 3000); // Record for 3 seconds
48
- }
49
- } catch (error) {
50
- console.error('Recording test failed:', error.message);
51
- }
52
- =======
53
1
  const MacRecorder = require('./index');
54
2
 
55
3
  function testScreenCaptureKit() {
@@ -105,5 +53,4 @@ function testScreenCaptureKit() {
105
53
  }
106
54
  }
107
55
 
108
- testScreenCaptureKit();
109
- >>>>>>> screencapture
56
+ testScreenCaptureKit();
@@ -1,19 +1,27 @@
1
1
  const { EventEmitter } = require("events");
2
2
  const path = require("path");
3
3
 
4
- // Native modülü yükle
4
+ // Native modülü yükle (arm64 prebuild öncelikli)
5
5
  let nativeBinding;
6
6
  try {
7
- nativeBinding = require("./build/Release/mac_recorder.node");
7
+ if (process.platform === "darwin" && process.arch === "arm64") {
8
+ nativeBinding = require("./prebuilds/darwin-arm64/node.napi.node");
9
+ } else {
10
+ nativeBinding = require("./build/Release/mac_recorder.node");
11
+ }
8
12
  } catch (error) {
9
13
  try {
10
- nativeBinding = require("./build/Debug/mac_recorder.node");
11
- } catch (debugError) {
12
- throw new Error(
13
- 'Native module not found. Please run "npm run build" to compile the native module.\n' +
14
- "Original error: " +
15
- error.message
16
- );
14
+ nativeBinding = require("./build/Release/mac_recorder.node");
15
+ } catch (_) {
16
+ try {
17
+ nativeBinding = require("./build/Debug/mac_recorder.node");
18
+ } catch (debugError) {
19
+ throw new Error(
20
+ 'Native module not found. Please run "npm run build" to compile the native module.\n' +
21
+ "Original error: " +
22
+ error.message
23
+ );
24
+ }
17
25
  }
18
26
  }
19
27
 
@@ -40,11 +48,11 @@ class WindowSelector extends EventEmitter {
40
48
  try {
41
49
  // Native window selection başlat
42
50
  const success = nativeBinding.startWindowSelection();
43
-
51
+
44
52
  if (success) {
45
53
  this.isSelecting = true;
46
54
  this.selectedWindow = null;
47
-
55
+
48
56
  // Status polling timer başlat (higher frequency for overlay updates)
49
57
  this.selectionTimer = setInterval(() => {
50
58
  this.checkSelectionStatus();
@@ -72,7 +80,7 @@ class WindowSelector extends EventEmitter {
72
80
  return new Promise((resolve, reject) => {
73
81
  try {
74
82
  const success = nativeBinding.stopWindowSelection();
75
-
83
+
76
84
  // Timer'ı durdur
77
85
  if (this.selectionTimer) {
78
86
  clearInterval(this.selectionTimer);
@@ -98,14 +106,14 @@ class WindowSelector extends EventEmitter {
98
106
 
99
107
  try {
100
108
  const status = nativeBinding.getWindowSelectionStatus();
101
-
109
+
102
110
  // Seçim tamamlandı mı kontrol et
103
111
  if (status.hasSelectedWindow && !this.selectedWindow) {
104
112
  const windowInfo = nativeBinding.getSelectedWindowInfo();
105
113
  if (windowInfo) {
106
114
  this.selectedWindow = windowInfo;
107
115
  this.isSelecting = false;
108
-
116
+
109
117
  // Timer'ı durdur
110
118
  if (this.selectionTimer) {
111
119
  clearInterval(this.selectionTimer);
@@ -121,17 +129,20 @@ class WindowSelector extends EventEmitter {
121
129
  if (this.lastStatus) {
122
130
  const lastWindow = this.lastStatus.currentWindow;
123
131
  const currentWindow = status.currentWindow;
124
-
132
+
125
133
  if (!lastWindow && currentWindow) {
126
134
  // Yeni pencere üstüne gelindi
127
135
  this.emit("windowEntered", currentWindow);
128
136
  } else if (lastWindow && !currentWindow) {
129
137
  // Pencere üstünden ayrıldı
130
138
  this.emit("windowLeft", lastWindow);
131
- } else if (lastWindow && currentWindow &&
132
- (lastWindow.id !== currentWindow.id ||
133
- lastWindow.title !== currentWindow.title ||
134
- lastWindow.appName !== currentWindow.appName)) {
139
+ } else if (
140
+ lastWindow &&
141
+ currentWindow &&
142
+ (lastWindow.id !== currentWindow.id ||
143
+ lastWindow.title !== currentWindow.title ||
144
+ lastWindow.appName !== currentWindow.appName)
145
+ ) {
135
146
  // Farklı bir pencereye geçildi
136
147
  this.emit("windowLeft", lastWindow);
137
148
  this.emit("windowEntered", currentWindow);
@@ -164,14 +175,14 @@ class WindowSelector extends EventEmitter {
164
175
  isSelecting: this.isSelecting && nativeStatus.isSelecting,
165
176
  hasSelectedWindow: !!this.selectedWindow,
166
177
  selectedWindow: this.selectedWindow,
167
- nativeStatus: nativeStatus
178
+ nativeStatus: nativeStatus,
168
179
  };
169
180
  } catch (error) {
170
181
  return {
171
182
  isSelecting: this.isSelecting,
172
183
  hasSelectedWindow: !!this.selectedWindow,
173
184
  selectedWindow: this.selectedWindow,
174
- error: error.message
185
+ error: error.message,
175
186
  };
176
187
  }
177
188
  }
@@ -205,7 +216,6 @@ class WindowSelector extends EventEmitter {
205
216
 
206
217
  // Seçimi başlat
207
218
  await this.startSelection();
208
-
209
219
  } catch (error) {
210
220
  this.removeAllListeners("windowSelected");
211
221
  this.removeAllListeners("error");
@@ -242,7 +252,9 @@ class WindowSelector extends EventEmitter {
242
252
  nativeBinding.setBringToFrontEnabled(enabled);
243
253
  // Only log if explicitly setting, not on startup
244
254
  if (arguments.length > 0) {
245
- console.log(`🔄 Auto bring-to-front: ${enabled ? 'ENABLED' : 'DISABLED'}`);
255
+ console.log(
256
+ `🔄 Auto bring-to-front: ${enabled ? "ENABLED" : "DISABLED"}`
257
+ );
246
258
  }
247
259
  } catch (error) {
248
260
  throw new Error(`Failed to set bring to front: ${error.message}`);
@@ -376,14 +388,14 @@ class WindowSelector extends EventEmitter {
376
388
  try {
377
389
  // Start screen selection
378
390
  await this.startScreenSelection();
379
-
391
+
380
392
  // Poll for selection completion
381
393
  return new Promise((resolve, reject) => {
382
394
  let isResolved = false;
383
-
395
+
384
396
  const checkSelection = () => {
385
397
  if (isResolved) return; // Prevent multiple resolutions
386
-
398
+
387
399
  const selectedScreen = this.getSelectedScreen();
388
400
  if (selectedScreen) {
389
401
  isResolved = true;
@@ -394,19 +406,19 @@ class WindowSelector extends EventEmitter {
394
406
  } else {
395
407
  // Selection was cancelled (probably ESC key)
396
408
  isResolved = true;
397
- reject(new Error('Screen selection was cancelled'));
409
+ reject(new Error("Screen selection was cancelled"));
398
410
  }
399
411
  };
400
-
412
+
401
413
  // Start polling
402
414
  checkSelection();
403
-
415
+
404
416
  // Timeout after 60 seconds
405
417
  setTimeout(() => {
406
418
  if (!isResolved) {
407
419
  isResolved = true;
408
420
  this.stopScreenSelection();
409
- reject(new Error('Screen selection timed out'));
421
+ reject(new Error("Screen selection timed out"));
410
422
  }
411
423
  }, 60000);
412
424
  });
@@ -430,7 +442,9 @@ class WindowSelector extends EventEmitter {
430
442
  const success = nativeBinding.showScreenRecordingPreview(screenInfo);
431
443
  return success;
432
444
  } catch (error) {
433
- throw new Error(`Failed to show screen recording preview: ${error.message}`);
445
+ throw new Error(
446
+ `Failed to show screen recording preview: ${error.message}`
447
+ );
434
448
  }
435
449
  }
436
450
 
@@ -443,7 +457,9 @@ class WindowSelector extends EventEmitter {
443
457
  const success = nativeBinding.hideScreenRecordingPreview();
444
458
  return success;
445
459
  } catch (error) {
446
- throw new Error(`Failed to hide screen recording preview: ${error.message}`);
460
+ throw new Error(
461
+ `Failed to hide screen recording preview: ${error.message}`
462
+ );
447
463
  }
448
464
  }
449
465
 
@@ -460,10 +476,10 @@ class WindowSelector extends EventEmitter {
460
476
  return {
461
477
  screenRecording: false,
462
478
  accessibility: false,
463
- error: error.message
479
+ error: error.message,
464
480
  };
465
481
  }
466
482
  }
467
483
  }
468
484
 
469
- module.exports = WindowSelector;
485
+ module.exports = WindowSelector;
Binary file
@@ -1,72 +0,0 @@
1
- /*
2
- Simple test runner: starts a 2s recording with ScreenCaptureKit and exclusions.
3
- */
4
- const fs = require("fs");
5
- const path = require("path");
6
- const MacRecorder = require("..");
7
-
8
- async function sleep(ms) {
9
- return new Promise((resolve) => setTimeout(resolve, ms));
10
- }
11
-
12
- async function main() {
13
- const recorder = new MacRecorder();
14
- const outDir = path.resolve(process.cwd(), "test-output");
15
- const outPath = path.join(outDir, `sc-exclude-${Date.now()}.mov`);
16
- await fs.promises.mkdir(outDir, { recursive: true });
17
-
18
- console.log("[TEST] Starting 2s recording with SC exclusions...");
19
- // Try to ensure overlays are not active in this process
20
-
21
- const perms = await recorder.checkPermissions();
22
- if (!perms?.screenRecording) {
23
- console.error(
24
- "[TEST] Screen Recording permission is not granted. Enable it in System Settings → Privacy & Security → Screen Recording for Terminal/Node, then re-run."
25
- );
26
- process.exit(1);
27
- }
28
-
29
- try {
30
- await recorder.startRecording(outPath, {
31
- useScreenCaptureKit: true,
32
- captureCursor: false,
33
- excludedAppBundleIds: ["com.apple.Safari"],
34
- });
35
- } catch (e) {
36
- console.error("[TEST] Failed to start recording:", e.message);
37
- process.exit(1);
38
- }
39
-
40
- await sleep(2000);
41
-
42
- try {
43
- const result = await recorder.stopRecording();
44
- console.log("[TEST] Stopped. Result:", result);
45
- } catch (e) {
46
- console.error("[TEST] Failed to stop recording:", e.message);
47
- process.exit(1);
48
- }
49
-
50
- // SCRecordingOutput write may be async; wait up to 10s for the file
51
- const deadline = Date.now() + 10000;
52
- let stats = null;
53
- while (Date.now() < deadline) {
54
- if (fs.existsSync(outPath)) {
55
- stats = fs.statSync(outPath);
56
- if (stats.size > 0) break;
57
- }
58
- await sleep(200);
59
- }
60
-
61
- if (stats && fs.existsSync(outPath)) {
62
- console.log(`[TEST] Output saved: ${outPath} (${stats.size} bytes)`);
63
- } else {
64
- console.error("[TEST] Output file not found or empty:", outPath);
65
- process.exit(1);
66
- }
67
- }
68
-
69
- main().catch((e) => {
70
- console.error("[TEST] Unhandled error:", e);
71
- process.exit(1);
72
- });
@@ -1,222 +0,0 @@
1
- #import "screen_capture_kit.h"
2
- #import <ScreenCaptureKit/ScreenCaptureKit.h>
3
- #import <AVFoundation/AVFoundation.h>
4
-
5
- static SCStream *g_scStream = nil;
6
- static SCRecordingOutput *g_scRecordingOutput = nil;
7
- static NSURL *g_outputURL = nil;
8
- static ScreenCaptureKitRecorder *g_scDelegate = nil;
9
- static NSArray<SCRunningApplication *> *g_appsToExclude = nil;
10
- static NSArray<SCWindow *> *g_windowsToExclude = nil;
11
-
12
- @interface ScreenCaptureKitRecorder () <SCRecordingOutputDelegate>
13
- @end
14
-
15
- @implementation ScreenCaptureKitRecorder
16
-
17
- + (BOOL)isScreenCaptureKitAvailable {
18
- if (@available(macOS 14.0, *)) {
19
- Class streamClass = NSClassFromString(@"SCStream");
20
- Class recordingOutputClass = NSClassFromString(@"SCRecordingOutput");
21
- return (streamClass != nil && recordingOutputClass != nil);
22
- }
23
- return NO;
24
- }
25
-
26
- + (BOOL)isRecording {
27
- return g_scStream != nil;
28
- }
29
-
30
- + (BOOL)startRecordingWithConfiguration:(NSDictionary *)config
31
- delegate:(id)delegate
32
- error:(NSError **)error {
33
- if (![self isScreenCaptureKitAvailable]) {
34
- if (error) {
35
- *error = [NSError errorWithDomain:@"ScreenCaptureKitRecorder"
36
- code:-1
37
- userInfo:@{NSLocalizedDescriptionKey: @"ScreenCaptureKit not available"}];
38
- }
39
- return NO;
40
- }
41
-
42
- if (g_scStream) {
43
- return NO;
44
- }
45
-
46
- @try {
47
- SCShareableContent *content = nil;
48
- if (@available(macOS 13.0, *)) {
49
- NSLog(@"[SCK] Fetching shareable content...");
50
- content = [SCShareableContent currentShareableContent];
51
- }
52
- if (!content) {
53
- if (error) { *error = [NSError errorWithDomain:@"ScreenCaptureKitRecorder" code:-2 userInfo:@{NSLocalizedDescriptionKey:@"Failed to get shareable content"}]; }
54
- NSLog(@"[SCK] Failed to get shareable content");
55
- return NO;
56
- }
57
-
58
- NSNumber *displayIdNumber = config[@"displayId"]; // CGDirectDisplayID
59
- NSDictionary *captureArea = config[@"captureArea"]; // {x,y,width,height}
60
- NSNumber *captureCursorNum = config[@"captureCursor"];
61
- NSNumber *includeMicNum = config[@"includeMicrophone"];
62
- NSNumber *includeSystemAudioNum = config[@"includeSystemAudio"];
63
- NSArray<NSString *> *excludedBundleIds = config[@"excludedAppBundleIds"];
64
- NSArray<NSNumber *> *excludedPIDs = config[@"excludedPIDs"];
65
- NSArray<NSNumber *> *excludedWindowIds = config[@"excludedWindowIds"];
66
- NSString *outputPath = config[@"outputPath"];
67
-
68
- SCDisplay *targetDisplay = nil;
69
- if (displayIdNumber) {
70
- uint32_t wanted = (uint32_t)displayIdNumber.unsignedIntValue;
71
- for (SCDisplay *d in content.displays) {
72
- if (d.displayID == wanted) { targetDisplay = d; break; }
73
- }
74
- }
75
- if (!targetDisplay) {
76
- targetDisplay = content.displays.firstObject;
77
- if (!targetDisplay) { NSLog(@"[SCK] No displays found"); return NO; }
78
- }
79
- NSLog(@"[SCK] Using displayID=%u", targetDisplay.displayID);
80
-
81
- NSMutableArray<SCRunningApplication*> *appsToExclude = [NSMutableArray array];
82
- if (excludedBundleIds.count > 0) {
83
- for (SCRunningApplication *app in content.applications) {
84
- if ([excludedBundleIds containsObject:app.bundleIdentifier]) {
85
- [appsToExclude addObject:app];
86
- }
87
- }
88
- }
89
- if (excludedPIDs.count > 0) {
90
- for (SCRunningApplication *app in content.applications) {
91
- if ([excludedPIDs containsObject:@(app.processID)]) {
92
- [appsToExclude addObject:app];
93
- }
94
- }
95
- }
96
-
97
- NSMutableArray<SCWindow*> *windowsToExclude = [NSMutableArray array];
98
- if (excludedWindowIds.count > 0) {
99
- for (SCWindow *w in content.windows) {
100
- if ([excludedWindowIds containsObject:@(w.windowID)]) {
101
- [windowsToExclude addObject:w];
102
- }
103
- }
104
- }
105
-
106
- // Keep strong references to excluded items for the lifetime of the stream
107
- g_appsToExclude = [appsToExclude copy];
108
- g_windowsToExclude = [windowsToExclude copy];
109
-
110
- SCContentFilter *filter = nil;
111
- if (appsToExclude.count > 0) {
112
- if (@available(macOS 13.0, *)) {
113
- filter = [[SCContentFilter alloc] initWithDisplay:targetDisplay excludingApplications:(g_appsToExclude ?: @[]) exceptingWindows:(g_windowsToExclude ?: @[])];
114
- }
115
- }
116
- if (!filter) {
117
- if (@available(macOS 13.0, *)) {
118
- filter = [[SCContentFilter alloc] initWithDisplay:targetDisplay excludingWindows:(g_windowsToExclude ?: @[])];
119
- }
120
- }
121
- if (!filter) { NSLog(@"[SCK] Failed to create filter"); return NO; }
122
-
123
- SCStreamConfiguration *cfg = [[SCStreamConfiguration alloc] init];
124
- if (captureArea && captureArea[@"width"] && captureArea[@"height"]) {
125
- CGRect displayFrame = targetDisplay.frame;
126
- double x = [captureArea[@"x"] doubleValue];
127
- double yBottom = [captureArea[@"y"] doubleValue];
128
- double w = [captureArea[@"width"] doubleValue];
129
- double h = [captureArea[@"height"] doubleValue];
130
-
131
- // Convert bottom-left origin (used by legacy path) to top-left for SC
132
- double y = displayFrame.size.height - yBottom - h;
133
-
134
- // Clamp to display bounds to avoid invalid sourceRect
135
- if (w < 1) w = 1; if (h < 1) h = 1;
136
- if (x < 0) x = 0; if (y < 0) y = 0;
137
- if (x + w > displayFrame.size.width) w = MAX(1, displayFrame.size.width - x);
138
- if (y + h > displayFrame.size.height) h = MAX(1, displayFrame.size.height - y);
139
-
140
- CGRect src = CGRectMake(x, y, w, h);
141
- cfg.sourceRect = src;
142
- cfg.width = (int)src.size.width;
143
- cfg.height = (int)src.size.height;
144
- } else {
145
- CGRect displayFrame = targetDisplay.frame;
146
- cfg.width = (int)displayFrame.size.width;
147
- cfg.height = (int)displayFrame.size.height;
148
- }
149
- cfg.showsCursor = captureCursorNum.boolValue;
150
- if (includeMicNum || includeSystemAudioNum) {
151
- if (@available(macOS 13.0, *)) {
152
- cfg.capturesAudio = YES;
153
- }
154
- }
155
-
156
- if (@available(macOS 15.0, *)) {
157
- g_scStream = [[SCStream alloc] initWithFilter:filter configuration:cfg delegate:nil];
158
- NSLog(@"[SCK] Stream created. w=%d h=%d cursor=%@ audio=%@", cfg.width, cfg.height, cfg.showsCursor?@"YES":@"NO", (@available(macOS 13.0, *) ? (cfg.capturesAudio?@"YES":@"NO") : @"N/A"));
159
-
160
- SCRecordingOutputConfiguration *recCfg = [[SCRecordingOutputConfiguration alloc] init];
161
- g_outputURL = [NSURL fileURLWithPath:outputPath];
162
- recCfg.outputURL = g_outputURL;
163
- recCfg.outputFileType = AVFileTypeQuickTimeMovie;
164
-
165
- id<SCRecordingOutputDelegate> delegateObject = (id<SCRecordingOutputDelegate>)delegate;
166
- if (!delegateObject) {
167
- if (!g_scDelegate) {
168
- g_scDelegate = [[ScreenCaptureKitRecorder alloc] init];
169
- }
170
- delegateObject = (id<SCRecordingOutputDelegate>)g_scDelegate;
171
- }
172
- g_scRecordingOutput = [[SCRecordingOutput alloc] initWithConfiguration:recCfg delegate:delegateObject];
173
-
174
- NSError *addErr = nil;
175
- BOOL added = [g_scStream addRecordingOutput:g_scRecordingOutput error:&addErr];
176
- if (!added) {
177
- NSLog(@"[SCK] addRecordingOutput failed: %@", addErr.localizedDescription);
178
- if (error) { *error = addErr ?: [NSError errorWithDomain:@"ScreenCaptureKitRecorder" code:-3 userInfo:nil]; }
179
- g_scRecordingOutput = nil; g_scStream = nil; g_outputURL = nil;
180
- return NO;
181
- }
182
-
183
- [g_scStream startCaptureWithCompletionHandler:^(NSError * _Nullable err) {
184
- if (err) {
185
- NSLog(@"[SCK] startCapture error: %@", err.localizedDescription);
186
- [g_scStream removeRecordingOutput:g_scRecordingOutput error:nil];
187
- g_scRecordingOutput = nil; g_scStream = nil; g_outputURL = nil;
188
- } else {
189
- NSLog(@"[SCK] startCapture OK");
190
- }
191
- }];
192
- // Return immediately; capture will start asynchronously
193
- return YES;
194
- }
195
-
196
- return NO;
197
- } @catch (__unused NSException *ex) {
198
- return NO;
199
- }
200
- }
201
-
202
- + (void)stopRecording {
203
- if (!g_scStream) { return; }
204
- @try {
205
- dispatch_semaphore_t sem = dispatch_semaphore_create(0);
206
- [g_scStream stopCaptureWithCompletionHandler:^(NSError * _Nullable error) {
207
- dispatch_semaphore_signal(sem);
208
- }];
209
- dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
210
- } @catch (__unused NSException *ex) {
211
- }
212
- if (g_scRecordingOutput) {
213
- [g_scStream removeRecordingOutput:g_scRecordingOutput error:nil];
214
- }
215
- g_scRecordingOutput = nil;
216
- g_scStream = nil;
217
- g_outputURL = nil;
218
- }
219
-
220
- @end
221
-
222
-