node-mac-recorder 1.2.3 → 1.2.5
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/README.md +3 -2
- package/capture-test.js +87 -0
- package/index.js +77 -28
- package/list-test.js +46 -0
- package/package.json +1 -1
- package/src/mac_recorder.mm +124 -53
- package/src/screen_capture.h +19 -0
package/README.md
CHANGED
|
@@ -92,6 +92,7 @@ await recorder.startRecording("./recording.mov", {
|
|
|
92
92
|
// Audio Controls
|
|
93
93
|
includeMicrophone: false, // Enable microphone (default: false)
|
|
94
94
|
includeSystemAudio: true, // Enable system audio (default: true)
|
|
95
|
+
audioDeviceId: "device-id", // Specific audio input device (default: system default)
|
|
95
96
|
|
|
96
97
|
// Display & Window Selection
|
|
97
98
|
displayId: 0, // Display index (null = main display)
|
|
@@ -159,14 +160,14 @@ console.log(displays);
|
|
|
159
160
|
|
|
160
161
|
#### `getAudioDevices()`
|
|
161
162
|
|
|
162
|
-
|
|
163
|
+
Returns a list of available audio input devices.
|
|
163
164
|
|
|
164
165
|
```javascript
|
|
165
166
|
const devices = await recorder.getAudioDevices();
|
|
166
167
|
console.log(devices);
|
|
167
168
|
// [
|
|
168
169
|
// {
|
|
169
|
-
// id: "device-id
|
|
170
|
+
// id: "device-id",
|
|
170
171
|
// name: "Built-in Microphone",
|
|
171
172
|
// manufacturer: "Apple Inc.",
|
|
172
173
|
// isDefault: true
|
package/capture-test.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const MacRecorder = require("./");
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
|
|
5
|
+
async function saveBase64Image(base64String, filePath) {
|
|
6
|
+
// Remove the data:image/png;base64, prefix if it exists
|
|
7
|
+
const base64Data = base64String.replace(/^data:image\/png;base64,/, "");
|
|
8
|
+
|
|
9
|
+
// Create directory if it doesn't exist
|
|
10
|
+
const dir = path.dirname(filePath);
|
|
11
|
+
if (!fs.existsSync(dir)) {
|
|
12
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Write the file
|
|
16
|
+
fs.writeFileSync(filePath, base64Data, "base64");
|
|
17
|
+
console.log(`✅ Saved image to: ${filePath}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function captureTest() {
|
|
21
|
+
const recorder = new MacRecorder();
|
|
22
|
+
|
|
23
|
+
// Create output directory
|
|
24
|
+
const outputDir = path.join(__dirname, "thumbnails");
|
|
25
|
+
if (!fs.existsSync(outputDir)) {
|
|
26
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
console.log("📸 Testing Display Capture");
|
|
30
|
+
|
|
31
|
+
// Get displays
|
|
32
|
+
const displays = await recorder.getDisplays();
|
|
33
|
+
console.log(`Found ${displays.length} displays`);
|
|
34
|
+
|
|
35
|
+
// Capture each display
|
|
36
|
+
for (const display of displays) {
|
|
37
|
+
console.log(
|
|
38
|
+
`\nCapturing display ${display.id} (${display.width}x${display.height})`
|
|
39
|
+
);
|
|
40
|
+
try {
|
|
41
|
+
const thumbnail = await recorder.getDisplayThumbnail(display.id, {
|
|
42
|
+
maxWidth: 800,
|
|
43
|
+
maxHeight: 600,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const fileName = `display_${display.id}.png`;
|
|
47
|
+
const filePath = path.join(outputDir, fileName);
|
|
48
|
+
await saveBase64Image(thumbnail, filePath);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error(`Failed to capture display ${display.id}:`, error);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log("\n📸 Testing Window Capture");
|
|
55
|
+
|
|
56
|
+
// Get windows
|
|
57
|
+
const windows = await recorder.getWindows();
|
|
58
|
+
console.log(`Found ${windows.length} windows`);
|
|
59
|
+
|
|
60
|
+
// Capture each window
|
|
61
|
+
for (const window of windows) {
|
|
62
|
+
console.log(
|
|
63
|
+
`\nCapturing window "${window.appName}" (${window.width}x${window.height})`
|
|
64
|
+
);
|
|
65
|
+
try {
|
|
66
|
+
const thumbnail = await recorder.getWindowThumbnail(window.id, {
|
|
67
|
+
maxWidth: 800,
|
|
68
|
+
maxHeight: 600,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const fileName = `window_${window.id}_${window.appName.replace(
|
|
72
|
+
/[^a-z0-9]/gi,
|
|
73
|
+
"_"
|
|
74
|
+
)}.png`;
|
|
75
|
+
const filePath = path.join(outputDir, fileName);
|
|
76
|
+
await saveBase64Image(thumbnail, filePath);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error(`Failed to capture window "${window.appName}":`, error);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.log(
|
|
83
|
+
"\n✅ Test completed. Check the thumbnails directory for results."
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
captureTest().catch(console.error);
|
package/index.js
CHANGED
|
@@ -69,31 +69,17 @@ class MacRecorder extends EventEmitter {
|
|
|
69
69
|
* macOS ekranlarını listeler
|
|
70
70
|
*/
|
|
71
71
|
async getDisplays() {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
? `${display.width}x${display.height}`
|
|
84
|
-
: "Unknown",
|
|
85
|
-
width: typeof display === "object" ? display.width : null,
|
|
86
|
-
height: typeof display === "object" ? display.height : null,
|
|
87
|
-
x: typeof display === "object" ? display.x : 0,
|
|
88
|
-
y: typeof display === "object" ? display.y : 0,
|
|
89
|
-
isPrimary:
|
|
90
|
-
typeof display === "object" ? display.isPrimary : index === 0,
|
|
91
|
-
}));
|
|
92
|
-
resolve(formattedDisplays);
|
|
93
|
-
} catch (error) {
|
|
94
|
-
reject(error);
|
|
95
|
-
}
|
|
96
|
-
});
|
|
72
|
+
const displays = nativeBinding.getDisplays();
|
|
73
|
+
return displays.map((display, index) => ({
|
|
74
|
+
id: display.id, // Use the actual display ID from native code
|
|
75
|
+
name: display.name,
|
|
76
|
+
width: display.width,
|
|
77
|
+
height: display.height,
|
|
78
|
+
x: display.x,
|
|
79
|
+
y: display.y,
|
|
80
|
+
isPrimary: display.isPrimary,
|
|
81
|
+
resolution: `${display.width}x${display.height}`,
|
|
82
|
+
}));
|
|
97
83
|
}
|
|
98
84
|
|
|
99
85
|
/**
|
|
@@ -113,8 +99,16 @@ class MacRecorder extends EventEmitter {
|
|
|
113
99
|
/**
|
|
114
100
|
* Kayıt seçeneklerini ayarlar
|
|
115
101
|
*/
|
|
116
|
-
setOptions(options) {
|
|
117
|
-
this.options = {
|
|
102
|
+
setOptions(options = {}) {
|
|
103
|
+
this.options = {
|
|
104
|
+
includeMicrophone: options.includeMicrophone || false,
|
|
105
|
+
includeSystemAudio: options.includeSystemAudio !== false, // Default true
|
|
106
|
+
captureCursor: options.captureCursor || false,
|
|
107
|
+
displayId: options.displayId || null, // null = ana ekran
|
|
108
|
+
windowId: options.windowId || null, // null = tam ekran
|
|
109
|
+
audioDeviceId: options.audioDeviceId || null, // null = default device
|
|
110
|
+
captureArea: options.captureArea || null,
|
|
111
|
+
};
|
|
118
112
|
}
|
|
119
113
|
|
|
120
114
|
/**
|
|
@@ -233,6 +227,7 @@ class MacRecorder extends EventEmitter {
|
|
|
233
227
|
captureCursor: this.options.captureCursor || false,
|
|
234
228
|
displayId: this.options.displayId || null, // null = ana ekran
|
|
235
229
|
windowId: this.options.windowId || null, // null = tam ekran
|
|
230
|
+
audioDeviceId: this.options.audioDeviceId || null, // null = default device
|
|
236
231
|
};
|
|
237
232
|
|
|
238
233
|
// Manuel captureArea varsa onu kullan
|
|
@@ -404,8 +399,16 @@ class MacRecorder extends EventEmitter {
|
|
|
404
399
|
|
|
405
400
|
return new Promise((resolve, reject) => {
|
|
406
401
|
try {
|
|
402
|
+
// Get all displays first to validate the ID
|
|
403
|
+
const displays = nativeBinding.getDisplays();
|
|
404
|
+
const display = displays.find((d) => d.id === displayId);
|
|
405
|
+
|
|
406
|
+
if (!display) {
|
|
407
|
+
throw new Error(`Display with ID ${displayId} not found`);
|
|
408
|
+
}
|
|
409
|
+
|
|
407
410
|
const base64Image = nativeBinding.getDisplayThumbnail(
|
|
408
|
-
|
|
411
|
+
display.id, // Use the actual CGDirectDisplayID
|
|
409
412
|
maxWidth,
|
|
410
413
|
maxHeight
|
|
411
414
|
);
|
|
@@ -586,6 +589,52 @@ class MacRecorder extends EventEmitter {
|
|
|
586
589
|
nativeModule: "mac_recorder.node",
|
|
587
590
|
};
|
|
588
591
|
}
|
|
592
|
+
|
|
593
|
+
async getDisplaysWithThumbnails(options = {}) {
|
|
594
|
+
const displays = await this.getDisplays();
|
|
595
|
+
|
|
596
|
+
// Get thumbnails for each display
|
|
597
|
+
const displayPromises = displays.map(async (display) => {
|
|
598
|
+
try {
|
|
599
|
+
const thumbnail = await this.getDisplayThumbnail(display.id, options);
|
|
600
|
+
return {
|
|
601
|
+
...display,
|
|
602
|
+
thumbnail,
|
|
603
|
+
};
|
|
604
|
+
} catch (error) {
|
|
605
|
+
return {
|
|
606
|
+
...display,
|
|
607
|
+
thumbnail: null,
|
|
608
|
+
thumbnailError: error.message,
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
return Promise.all(displayPromises);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
async getWindowsWithThumbnails(options = {}) {
|
|
617
|
+
const windows = await this.getWindows();
|
|
618
|
+
|
|
619
|
+
// Get thumbnails for each window
|
|
620
|
+
const windowPromises = windows.map(async (window) => {
|
|
621
|
+
try {
|
|
622
|
+
const thumbnail = await this.getWindowThumbnail(window.id, options);
|
|
623
|
+
return {
|
|
624
|
+
...window,
|
|
625
|
+
thumbnail,
|
|
626
|
+
};
|
|
627
|
+
} catch (error) {
|
|
628
|
+
return {
|
|
629
|
+
...window,
|
|
630
|
+
thumbnail: null,
|
|
631
|
+
thumbnailError: error.message,
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
return Promise.all(windowPromises);
|
|
637
|
+
}
|
|
589
638
|
}
|
|
590
639
|
|
|
591
640
|
module.exports = MacRecorder;
|
package/list-test.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const MacRecorder = require("./");
|
|
2
|
+
|
|
3
|
+
async function testListing() {
|
|
4
|
+
const recorder = new MacRecorder();
|
|
5
|
+
|
|
6
|
+
console.log("📺 Testing Displays with Thumbnails");
|
|
7
|
+
const displays = await recorder.getDisplaysWithThumbnails({
|
|
8
|
+
maxWidth: 200,
|
|
9
|
+
maxHeight: 150,
|
|
10
|
+
});
|
|
11
|
+
console.log(`Found ${displays.length} displays:`);
|
|
12
|
+
for (const display of displays) {
|
|
13
|
+
console.log(`\nDisplay ID: ${display.id}`);
|
|
14
|
+
console.log(`Resolution: ${display.width}x${display.height}`);
|
|
15
|
+
console.log(`Position: (${display.x}, ${display.y})`);
|
|
16
|
+
console.log(`Primary: ${display.isPrimary}`);
|
|
17
|
+
console.log(
|
|
18
|
+
`Thumbnail included: ${display.thumbnail.substring(0, 50)}... (${
|
|
19
|
+
display.thumbnail.length
|
|
20
|
+
} chars)`
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
console.log("\n🪟 Testing Windows with Thumbnails");
|
|
25
|
+
const windows = await recorder.getWindowsWithThumbnails({
|
|
26
|
+
maxWidth: 200,
|
|
27
|
+
maxHeight: 150,
|
|
28
|
+
});
|
|
29
|
+
console.log(`Found ${windows.length} windows:`);
|
|
30
|
+
for (const window of windows.slice(0, 3)) {
|
|
31
|
+
// Just show first 3 for brevity
|
|
32
|
+
console.log(`\nWindow: ${window.appName}`);
|
|
33
|
+
console.log(`ID: ${window.id}`);
|
|
34
|
+
console.log(`Size: ${window.width}x${window.height}`);
|
|
35
|
+
console.log(
|
|
36
|
+
`Thumbnail included: ${window.thumbnail.substring(0, 50)}... (${
|
|
37
|
+
window.thumbnail.length
|
|
38
|
+
} chars)`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
if (windows.length > 3) {
|
|
42
|
+
console.log(`\n... and ${windows.length - 3} more windows`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
testListing().catch(console.error);
|
package/package.json
CHANGED
package/src/mac_recorder.mm
CHANGED
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
#import <ImageIO/ImageIO.h>
|
|
8
8
|
#import <CoreAudio/CoreAudio.h>
|
|
9
9
|
|
|
10
|
+
// Import screen capture
|
|
11
|
+
#import "screen_capture.h"
|
|
12
|
+
|
|
10
13
|
// Cursor tracker function declarations
|
|
11
14
|
Napi::Object InitCursorTracker(Napi::Env env, Napi::Object exports);
|
|
12
15
|
|
|
@@ -67,6 +70,7 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
67
70
|
bool includeMicrophone = false; // Default olarak mikrofon kapalı
|
|
68
71
|
bool includeSystemAudio = true; // Default olarak sistem sesi açık
|
|
69
72
|
CGDirectDisplayID displayID = CGMainDisplayID(); // Default ana ekran
|
|
73
|
+
NSString *audioDeviceId = nil; // Default audio device ID
|
|
70
74
|
|
|
71
75
|
if (info.Length() > 1 && info[1].IsObject()) {
|
|
72
76
|
Napi::Object options = info[1].As<Napi::Object>();
|
|
@@ -94,6 +98,12 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
94
98
|
includeMicrophone = options.Get("includeMicrophone").As<Napi::Boolean>();
|
|
95
99
|
}
|
|
96
100
|
|
|
101
|
+
// Audio device ID
|
|
102
|
+
if (options.Has("audioDeviceId") && !options.Get("audioDeviceId").IsNull()) {
|
|
103
|
+
std::string deviceId = options.Get("audioDeviceId").As<Napi::String>().Utf8Value();
|
|
104
|
+
audioDeviceId = [NSString stringWithUTF8String:deviceId.c_str()];
|
|
105
|
+
}
|
|
106
|
+
|
|
97
107
|
// System audio
|
|
98
108
|
if (options.Has("includeSystemAudio")) {
|
|
99
109
|
includeSystemAudio = options.Get("includeSystemAudio").As<Napi::Boolean>();
|
|
@@ -151,12 +161,41 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
|
|
|
151
161
|
|
|
152
162
|
// Add microphone input if requested
|
|
153
163
|
if (includeMicrophone) {
|
|
154
|
-
AVCaptureDevice *audioDevice =
|
|
164
|
+
AVCaptureDevice *audioDevice = nil;
|
|
165
|
+
|
|
166
|
+
if (audioDeviceId) {
|
|
167
|
+
// Try to find the specified device
|
|
168
|
+
NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio];
|
|
169
|
+
NSLog(@"[DEBUG] Looking for audio device with ID: %@", audioDeviceId);
|
|
170
|
+
NSLog(@"[DEBUG] Available audio devices:");
|
|
171
|
+
for (AVCaptureDevice *device in devices) {
|
|
172
|
+
NSLog(@"[DEBUG] - Device: %@ (ID: %@)", device.localizedName, device.uniqueID);
|
|
173
|
+
if ([device.uniqueID isEqualToString:audioDeviceId]) {
|
|
174
|
+
NSLog(@"[DEBUG] Found matching device: %@", device.localizedName);
|
|
175
|
+
audioDevice = device;
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!audioDevice) {
|
|
181
|
+
NSLog(@"[DEBUG] Specified audio device not found, falling back to default");
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Fallback to default device if specified device not found
|
|
186
|
+
if (!audioDevice) {
|
|
187
|
+
audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
|
|
188
|
+
NSLog(@"[DEBUG] Using default audio device: %@ (ID: %@)", audioDevice.localizedName, audioDevice.uniqueID);
|
|
189
|
+
}
|
|
190
|
+
|
|
155
191
|
if (audioDevice) {
|
|
156
192
|
NSError *error;
|
|
157
193
|
g_audioInput = [[AVCaptureDeviceInput alloc] initWithDevice:audioDevice error:&error];
|
|
158
194
|
if (g_audioInput && [g_captureSession canAddInput:g_audioInput]) {
|
|
159
195
|
[g_captureSession addInput:g_audioInput];
|
|
196
|
+
NSLog(@"[DEBUG] Successfully added audio input device");
|
|
197
|
+
} else {
|
|
198
|
+
NSLog(@"[DEBUG] Failed to add audio input device: %@", error);
|
|
160
199
|
}
|
|
161
200
|
}
|
|
162
201
|
}
|
|
@@ -363,35 +402,20 @@ Napi::Value GetDisplays(const Napi::CallbackInfo& info) {
|
|
|
363
402
|
Napi::Env env = info.Env();
|
|
364
403
|
|
|
365
404
|
@try {
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
uint32_t displayCount;
|
|
369
|
-
CGGetActiveDisplayList(0, NULL, &displayCount);
|
|
370
|
-
|
|
371
|
-
CGDirectDisplayID *displayList = (CGDirectDisplayID *)malloc(displayCount * sizeof(CGDirectDisplayID));
|
|
372
|
-
CGGetActiveDisplayList(displayCount, displayList, &displayCount);
|
|
373
|
-
|
|
374
|
-
for (uint32_t i = 0; i < displayCount; i++) {
|
|
375
|
-
CGDirectDisplayID displayID = displayList[i];
|
|
376
|
-
CGRect bounds = CGDisplayBounds(displayID);
|
|
377
|
-
|
|
378
|
-
[displays addObject:@{
|
|
379
|
-
@"id": @(displayID),
|
|
380
|
-
@"name": [NSString stringWithFormat:@"Display %d", i + 1],
|
|
381
|
-
@"width": @(bounds.size.width),
|
|
382
|
-
@"height": @(bounds.size.height),
|
|
383
|
-
@"x": @(bounds.origin.x),
|
|
384
|
-
@"y": @(bounds.origin.y),
|
|
385
|
-
@"isPrimary": @(CGDisplayIsMain(displayID))
|
|
386
|
-
}];
|
|
387
|
-
}
|
|
405
|
+
NSArray *displays = [ScreenCapture getAvailableDisplays];
|
|
406
|
+
Napi::Array result = Napi::Array::New(env, displays.count);
|
|
388
407
|
|
|
389
|
-
|
|
408
|
+
NSLog(@"Found %lu displays", (unsigned long)displays.count);
|
|
390
409
|
|
|
391
|
-
// Convert to NAPI array
|
|
392
|
-
Napi::Array result = Napi::Array::New(env, displays.count);
|
|
393
410
|
for (NSUInteger i = 0; i < displays.count; i++) {
|
|
394
411
|
NSDictionary *display = displays[i];
|
|
412
|
+
NSLog(@"Display %lu: ID=%u, Name=%@, Size=%@x%@",
|
|
413
|
+
(unsigned long)i,
|
|
414
|
+
[display[@"id"] unsignedIntValue],
|
|
415
|
+
display[@"name"],
|
|
416
|
+
display[@"width"],
|
|
417
|
+
display[@"height"]);
|
|
418
|
+
|
|
395
419
|
Napi::Object displayObj = Napi::Object::New(env);
|
|
396
420
|
displayObj.Set("id", Napi::Number::New(env, [display[@"id"] unsignedIntValue]));
|
|
397
421
|
displayObj.Set("name", Napi::String::New(env, [display[@"name"] UTF8String]));
|
|
@@ -406,6 +430,7 @@ Napi::Value GetDisplays(const Napi::CallbackInfo& info) {
|
|
|
406
430
|
return result;
|
|
407
431
|
|
|
408
432
|
} @catch (NSException *exception) {
|
|
433
|
+
NSLog(@"Exception in GetDisplays: %@", exception);
|
|
409
434
|
return Napi::Array::New(env, 0);
|
|
410
435
|
}
|
|
411
436
|
}
|
|
@@ -536,10 +561,34 @@ Napi::Value GetDisplayThumbnail(const Napi::CallbackInfo& info) {
|
|
|
536
561
|
}
|
|
537
562
|
|
|
538
563
|
@try {
|
|
564
|
+
// Verify display exists
|
|
565
|
+
CGDirectDisplayID activeDisplays[32];
|
|
566
|
+
uint32_t displayCount;
|
|
567
|
+
CGError err = CGGetActiveDisplayList(32, activeDisplays, &displayCount);
|
|
568
|
+
|
|
569
|
+
if (err != kCGErrorSuccess) {
|
|
570
|
+
NSLog(@"Failed to get active display list: %d", err);
|
|
571
|
+
return env.Null();
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
bool displayFound = false;
|
|
575
|
+
for (uint32_t i = 0; i < displayCount; i++) {
|
|
576
|
+
if (activeDisplays[i] == displayID) {
|
|
577
|
+
displayFound = true;
|
|
578
|
+
break;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (!displayFound) {
|
|
583
|
+
NSLog(@"Display ID %u not found in active displays", displayID);
|
|
584
|
+
return env.Null();
|
|
585
|
+
}
|
|
586
|
+
|
|
539
587
|
// Create display image
|
|
540
588
|
CGImageRef displayImage = CGDisplayCreateImage(displayID);
|
|
541
589
|
|
|
542
590
|
if (!displayImage) {
|
|
591
|
+
NSLog(@"CGDisplayCreateImage failed for display ID: %u", displayID);
|
|
543
592
|
return env.Null();
|
|
544
593
|
}
|
|
545
594
|
|
|
@@ -547,6 +596,8 @@ Napi::Value GetDisplayThumbnail(const Napi::CallbackInfo& info) {
|
|
|
547
596
|
size_t originalWidth = CGImageGetWidth(displayImage);
|
|
548
597
|
size_t originalHeight = CGImageGetHeight(displayImage);
|
|
549
598
|
|
|
599
|
+
NSLog(@"Original dimensions: %zux%zu", originalWidth, originalHeight);
|
|
600
|
+
|
|
550
601
|
// Calculate scaled dimensions maintaining aspect ratio
|
|
551
602
|
double scaleX = (double)maxWidth / originalWidth;
|
|
552
603
|
double scaleY = (double)maxHeight / originalHeight;
|
|
@@ -555,6 +606,8 @@ Napi::Value GetDisplayThumbnail(const Napi::CallbackInfo& info) {
|
|
|
555
606
|
size_t thumbnailWidth = (size_t)(originalWidth * scale);
|
|
556
607
|
size_t thumbnailHeight = (size_t)(originalHeight * scale);
|
|
557
608
|
|
|
609
|
+
NSLog(@"Thumbnail dimensions: %zux%zu (scale: %f)", thumbnailWidth, thumbnailHeight, scale);
|
|
610
|
+
|
|
558
611
|
// Create scaled image
|
|
559
612
|
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
|
|
560
613
|
CGContextRef context = CGBitmapContextCreate(
|
|
@@ -564,43 +617,61 @@ Napi::Value GetDisplayThumbnail(const Napi::CallbackInfo& info) {
|
|
|
564
617
|
8,
|
|
565
618
|
thumbnailWidth * 4,
|
|
566
619
|
colorSpace,
|
|
567
|
-
kCGImageAlphaPremultipliedLast
|
|
620
|
+
kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big
|
|
568
621
|
);
|
|
569
622
|
|
|
570
|
-
if (context) {
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
CGColorSpaceRelease(colorSpace);
|
|
587
|
-
CGImageRelease(displayImage);
|
|
588
|
-
|
|
589
|
-
return Napi::String::New(env, base64Std);
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
CGImageRelease(thumbnailImage);
|
|
593
|
-
}
|
|
594
|
-
|
|
623
|
+
if (!context) {
|
|
624
|
+
NSLog(@"Failed to create bitmap context");
|
|
625
|
+
CGImageRelease(displayImage);
|
|
626
|
+
CGColorSpaceRelease(colorSpace);
|
|
627
|
+
return env.Null();
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Set interpolation quality for better scaling
|
|
631
|
+
CGContextSetInterpolationQuality(context, kCGInterpolationHigh);
|
|
632
|
+
|
|
633
|
+
// Draw the image
|
|
634
|
+
CGContextDrawImage(context, CGRectMake(0, 0, thumbnailWidth, thumbnailHeight), displayImage);
|
|
635
|
+
CGImageRef thumbnailImage = CGBitmapContextCreateImage(context);
|
|
636
|
+
|
|
637
|
+
if (!thumbnailImage) {
|
|
638
|
+
NSLog(@"Failed to create thumbnail image");
|
|
595
639
|
CGContextRelease(context);
|
|
640
|
+
CGImageRelease(displayImage);
|
|
641
|
+
CGColorSpaceRelease(colorSpace);
|
|
642
|
+
return env.Null();
|
|
596
643
|
}
|
|
597
644
|
|
|
645
|
+
// Convert to PNG data
|
|
646
|
+
NSBitmapImageRep *imageRep = [[NSBitmapImageRep alloc] initWithCGImage:thumbnailImage];
|
|
647
|
+
NSDictionary *properties = @{NSImageCompressionFactor: @0.8};
|
|
648
|
+
NSData *pngData = [imageRep representationUsingType:NSBitmapImageFileTypePNG properties:properties];
|
|
649
|
+
|
|
650
|
+
if (!pngData) {
|
|
651
|
+
NSLog(@"Failed to convert image to PNG data");
|
|
652
|
+
CGImageRelease(thumbnailImage);
|
|
653
|
+
CGContextRelease(context);
|
|
654
|
+
CGImageRelease(displayImage);
|
|
655
|
+
CGColorSpaceRelease(colorSpace);
|
|
656
|
+
return env.Null();
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Convert to Base64
|
|
660
|
+
NSString *base64String = [pngData base64EncodedStringWithOptions:0];
|
|
661
|
+
std::string base64Std = [base64String UTF8String];
|
|
662
|
+
|
|
663
|
+
NSLog(@"Successfully created thumbnail with base64 length: %lu", (unsigned long)base64Std.length());
|
|
664
|
+
|
|
665
|
+
// Cleanup
|
|
666
|
+
CGImageRelease(thumbnailImage);
|
|
667
|
+
CGContextRelease(context);
|
|
598
668
|
CGColorSpaceRelease(colorSpace);
|
|
599
669
|
CGImageRelease(displayImage);
|
|
600
670
|
|
|
601
|
-
return env
|
|
671
|
+
return Napi::String::New(env, base64Std);
|
|
602
672
|
|
|
603
673
|
} @catch (NSException *exception) {
|
|
674
|
+
NSLog(@"Exception in GetDisplayThumbnail: %@", exception);
|
|
604
675
|
return env.Null();
|
|
605
676
|
}
|
|
606
677
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#ifndef SCREEN_CAPTURE_H
|
|
2
|
+
#define SCREEN_CAPTURE_H
|
|
3
|
+
|
|
4
|
+
#import <Foundation/Foundation.h>
|
|
5
|
+
#import <CoreGraphics/CoreGraphics.h>
|
|
6
|
+
|
|
7
|
+
@interface ScreenCapture : NSObject
|
|
8
|
+
|
|
9
|
+
+ (NSArray *)getAvailableDisplays;
|
|
10
|
+
+ (BOOL)captureDisplay:(CGDirectDisplayID)displayID
|
|
11
|
+
toFile:(NSString *)filePath
|
|
12
|
+
rect:(CGRect)rect
|
|
13
|
+
includeCursor:(BOOL)includeCursor;
|
|
14
|
+
+ (CGImageRef)createScreenshotFromDisplay:(CGDirectDisplayID)displayID
|
|
15
|
+
rect:(CGRect)rect;
|
|
16
|
+
|
|
17
|
+
@end
|
|
18
|
+
|
|
19
|
+
#endif // SCREEN_CAPTURE_H
|