node-mac-recorder 1.2.2 ā 1.2.4
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/capture-test.js +87 -0
- package/cursor-data.json +307 -41
- package/cursor-test.js +21 -49
- package/index.js +66 -26
- package/list-test.js +46 -0
- package/package.json +1 -1
- package/src/cursor_tracker.mm +95 -29
- package/src/mac_recorder.mm +87 -52
- package/src/screen_capture.h +19 -0
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/cursor-data.json
CHANGED
|
@@ -1,93 +1,359 @@
|
|
|
1
1
|
[
|
|
2
2
|
{
|
|
3
|
-
"x":
|
|
4
|
-
"y":
|
|
5
|
-
"timestamp":
|
|
3
|
+
"x": 1947,
|
|
4
|
+
"y": 780,
|
|
5
|
+
"timestamp": 85,
|
|
6
|
+
"cursorType": "pointer",
|
|
7
|
+
"type": "move"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"x": 1950,
|
|
11
|
+
"y": 780,
|
|
12
|
+
"timestamp": 1476,
|
|
13
|
+
"cursorType": "pointer",
|
|
14
|
+
"type": "move"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"x": 1954,
|
|
18
|
+
"y": 778,
|
|
19
|
+
"timestamp": 1500,
|
|
20
|
+
"cursorType": "pointer",
|
|
21
|
+
"type": "move"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"x": 1958,
|
|
25
|
+
"y": 777,
|
|
26
|
+
"timestamp": 1518,
|
|
27
|
+
"cursorType": "pointer",
|
|
28
|
+
"type": "move"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"x": 1963,
|
|
32
|
+
"y": 775,
|
|
33
|
+
"timestamp": 1541,
|
|
34
|
+
"cursorType": "pointer",
|
|
35
|
+
"type": "move"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"x": 1967,
|
|
39
|
+
"y": 775,
|
|
40
|
+
"timestamp": 1595,
|
|
6
41
|
"cursorType": "default",
|
|
7
42
|
"type": "move"
|
|
8
43
|
},
|
|
9
44
|
{
|
|
10
|
-
"x":
|
|
11
|
-
"y":
|
|
12
|
-
"timestamp":
|
|
45
|
+
"x": 1971,
|
|
46
|
+
"y": 775,
|
|
47
|
+
"timestamp": 1596,
|
|
13
48
|
"cursorType": "default",
|
|
14
49
|
"type": "move"
|
|
15
50
|
},
|
|
16
51
|
{
|
|
17
|
-
"x":
|
|
18
|
-
"y":
|
|
19
|
-
"timestamp":
|
|
52
|
+
"x": 1973,
|
|
53
|
+
"y": 775,
|
|
54
|
+
"timestamp": 1617,
|
|
20
55
|
"cursorType": "default",
|
|
21
56
|
"type": "move"
|
|
22
57
|
},
|
|
23
58
|
{
|
|
24
|
-
"x":
|
|
25
|
-
"y":
|
|
26
|
-
"timestamp":
|
|
59
|
+
"x": 1975,
|
|
60
|
+
"y": 775,
|
|
61
|
+
"timestamp": 1659,
|
|
27
62
|
"cursorType": "default",
|
|
28
63
|
"type": "move"
|
|
29
64
|
},
|
|
30
65
|
{
|
|
31
|
-
"x":
|
|
32
|
-
"y":
|
|
33
|
-
"timestamp":
|
|
66
|
+
"x": 1977,
|
|
67
|
+
"y": 775,
|
|
68
|
+
"timestamp": 1679,
|
|
69
|
+
"cursorType": "pointer",
|
|
70
|
+
"type": "move"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"x": 1979,
|
|
74
|
+
"y": 775,
|
|
75
|
+
"timestamp": 1744,
|
|
76
|
+
"cursorType": "pointer",
|
|
77
|
+
"type": "move"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"x": 1979,
|
|
81
|
+
"y": 777,
|
|
82
|
+
"timestamp": 1807,
|
|
83
|
+
"cursorType": "pointer",
|
|
84
|
+
"type": "move"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"x": 1973,
|
|
88
|
+
"y": 782,
|
|
89
|
+
"timestamp": 1828,
|
|
34
90
|
"cursorType": "default",
|
|
35
91
|
"type": "move"
|
|
36
92
|
},
|
|
37
93
|
{
|
|
38
|
-
"x":
|
|
39
|
-
"y":
|
|
40
|
-
"timestamp":
|
|
94
|
+
"x": 1968,
|
|
95
|
+
"y": 787,
|
|
96
|
+
"timestamp": 1850,
|
|
41
97
|
"cursorType": "default",
|
|
42
98
|
"type": "move"
|
|
43
99
|
},
|
|
44
100
|
{
|
|
45
|
-
"x":
|
|
46
|
-
"y":
|
|
47
|
-
"timestamp":
|
|
101
|
+
"x": 1957,
|
|
102
|
+
"y": 793,
|
|
103
|
+
"timestamp": 1868,
|
|
104
|
+
"cursorType": "pointer",
|
|
105
|
+
"type": "move"
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"x": 1948,
|
|
109
|
+
"y": 799,
|
|
110
|
+
"timestamp": 1891,
|
|
111
|
+
"cursorType": "pointer",
|
|
112
|
+
"type": "move"
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"x": 1943,
|
|
116
|
+
"y": 802,
|
|
117
|
+
"timestamp": 1911,
|
|
48
118
|
"cursorType": "default",
|
|
49
119
|
"type": "move"
|
|
50
120
|
},
|
|
51
121
|
{
|
|
52
|
-
"x":
|
|
53
|
-
"y":
|
|
54
|
-
"timestamp":
|
|
122
|
+
"x": 1937,
|
|
123
|
+
"y": 804,
|
|
124
|
+
"timestamp": 1933,
|
|
125
|
+
"cursorType": "default",
|
|
126
|
+
"type": "move"
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"x": 1938,
|
|
130
|
+
"y": 800,
|
|
131
|
+
"timestamp": 2309,
|
|
132
|
+
"cursorType": "default",
|
|
133
|
+
"type": "move"
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"x": 1939,
|
|
137
|
+
"y": 799,
|
|
138
|
+
"timestamp": 2329,
|
|
139
|
+
"cursorType": "pointer",
|
|
140
|
+
"type": "move"
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"x": 1940,
|
|
144
|
+
"y": 797,
|
|
145
|
+
"timestamp": 2435,
|
|
146
|
+
"cursorType": "pointer",
|
|
147
|
+
"type": "move"
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"x": 1941,
|
|
151
|
+
"y": 794,
|
|
152
|
+
"timestamp": 2477,
|
|
153
|
+
"cursorType": "pointer",
|
|
154
|
+
"type": "move"
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"x": 1941,
|
|
158
|
+
"y": 792,
|
|
159
|
+
"timestamp": 2500,
|
|
160
|
+
"cursorType": "pointer",
|
|
161
|
+
"type": "move"
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
"x": 1942,
|
|
165
|
+
"y": 788,
|
|
166
|
+
"timestamp": 2518,
|
|
167
|
+
"cursorType": "pointer",
|
|
168
|
+
"type": "move"
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
"x": 1943,
|
|
172
|
+
"y": 786,
|
|
173
|
+
"timestamp": 2560,
|
|
174
|
+
"cursorType": "pointer",
|
|
175
|
+
"type": "move"
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
"x": 1945,
|
|
179
|
+
"y": 784,
|
|
180
|
+
"timestamp": 2707,
|
|
181
|
+
"cursorType": "pointer",
|
|
182
|
+
"type": "move"
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
"x": 1945,
|
|
186
|
+
"y": 796,
|
|
187
|
+
"timestamp": 3544,
|
|
188
|
+
"cursorType": "pointer",
|
|
189
|
+
"type": "move"
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
"x": 1945,
|
|
193
|
+
"y": 817,
|
|
194
|
+
"timestamp": 3568,
|
|
55
195
|
"cursorType": "default",
|
|
56
196
|
"type": "move"
|
|
57
197
|
},
|
|
58
198
|
{
|
|
59
|
-
"x":
|
|
60
|
-
"y":
|
|
61
|
-
"timestamp":
|
|
199
|
+
"x": 1951,
|
|
200
|
+
"y": 863,
|
|
201
|
+
"timestamp": 3589,
|
|
62
202
|
"cursorType": "default",
|
|
63
203
|
"type": "move"
|
|
64
204
|
},
|
|
65
205
|
{
|
|
66
|
-
"x":
|
|
67
|
-
"y":
|
|
68
|
-
"timestamp":
|
|
206
|
+
"x": 1956,
|
|
207
|
+
"y": 925,
|
|
208
|
+
"timestamp": 3609,
|
|
69
209
|
"cursorType": "default",
|
|
70
|
-
"type": "
|
|
210
|
+
"type": "move"
|
|
71
211
|
},
|
|
72
212
|
{
|
|
73
|
-
"x":
|
|
74
|
-
"y":
|
|
75
|
-
"timestamp":
|
|
213
|
+
"x": 1966,
|
|
214
|
+
"y": 983,
|
|
215
|
+
"timestamp": 3630,
|
|
76
216
|
"cursorType": "default",
|
|
77
217
|
"type": "move"
|
|
78
218
|
},
|
|
79
219
|
{
|
|
80
|
-
"x":
|
|
81
|
-
"y":
|
|
82
|
-
"timestamp":
|
|
220
|
+
"x": 1970,
|
|
221
|
+
"y": 1006,
|
|
222
|
+
"timestamp": 3650,
|
|
223
|
+
"cursorType": "text",
|
|
224
|
+
"type": "move"
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
"x": 1978,
|
|
228
|
+
"y": 1042,
|
|
229
|
+
"timestamp": 3675,
|
|
83
230
|
"cursorType": "default",
|
|
84
|
-
"type": "
|
|
231
|
+
"type": "move"
|
|
85
232
|
},
|
|
86
233
|
{
|
|
87
|
-
"x":
|
|
88
|
-
"y":
|
|
89
|
-
"timestamp":
|
|
234
|
+
"x": 1983,
|
|
235
|
+
"y": 1061,
|
|
236
|
+
"timestamp": 3693,
|
|
90
237
|
"cursorType": "default",
|
|
91
238
|
"type": "move"
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
"x": 1986,
|
|
242
|
+
"y": 1072,
|
|
243
|
+
"timestamp": 3713,
|
|
244
|
+
"cursorType": "text",
|
|
245
|
+
"type": "move"
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
"x": 1990,
|
|
249
|
+
"y": 1082,
|
|
250
|
+
"timestamp": 3734,
|
|
251
|
+
"cursorType": "text",
|
|
252
|
+
"type": "move"
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
"x": 1991,
|
|
256
|
+
"y": 1086,
|
|
257
|
+
"timestamp": 3755,
|
|
258
|
+
"cursorType": "text",
|
|
259
|
+
"type": "move"
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
"x": 1993,
|
|
263
|
+
"y": 1091,
|
|
264
|
+
"timestamp": 3776,
|
|
265
|
+
"cursorType": "text",
|
|
266
|
+
"type": "move"
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
"x": 1994,
|
|
270
|
+
"y": 1096,
|
|
271
|
+
"timestamp": 3797,
|
|
272
|
+
"cursorType": "text",
|
|
273
|
+
"type": "move"
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
"x": 1995,
|
|
277
|
+
"y": 1099,
|
|
278
|
+
"timestamp": 3818,
|
|
279
|
+
"cursorType": "text",
|
|
280
|
+
"type": "move"
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
"x": 1997,
|
|
284
|
+
"y": 1106,
|
|
285
|
+
"timestamp": 3841,
|
|
286
|
+
"cursorType": "text",
|
|
287
|
+
"type": "move"
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
"x": 1997,
|
|
291
|
+
"y": 1109,
|
|
292
|
+
"timestamp": 3859,
|
|
293
|
+
"cursorType": "text",
|
|
294
|
+
"type": "move"
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
"x": 1998,
|
|
298
|
+
"y": 1116,
|
|
299
|
+
"timestamp": 3882,
|
|
300
|
+
"cursorType": "text",
|
|
301
|
+
"type": "move"
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
"x": 2000,
|
|
305
|
+
"y": 1120,
|
|
306
|
+
"timestamp": 3902,
|
|
307
|
+
"cursorType": "text",
|
|
308
|
+
"type": "move"
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
"x": 2000,
|
|
312
|
+
"y": 1123,
|
|
313
|
+
"timestamp": 3924,
|
|
314
|
+
"cursorType": "text",
|
|
315
|
+
"type": "move"
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
"x": 2001,
|
|
319
|
+
"y": 1126,
|
|
320
|
+
"timestamp": 3945,
|
|
321
|
+
"cursorType": "text",
|
|
322
|
+
"type": "move"
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
"x": 2001,
|
|
326
|
+
"y": 1129,
|
|
327
|
+
"timestamp": 3967,
|
|
328
|
+
"cursorType": "text",
|
|
329
|
+
"type": "move"
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
"x": 2002,
|
|
333
|
+
"y": 1135,
|
|
334
|
+
"timestamp": 3986,
|
|
335
|
+
"cursorType": "pointer",
|
|
336
|
+
"type": "move"
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
"x": 2002,
|
|
340
|
+
"y": 1142,
|
|
341
|
+
"timestamp": 4007,
|
|
342
|
+
"cursorType": "pointer",
|
|
343
|
+
"type": "move"
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
"x": 2002,
|
|
347
|
+
"y": 1146,
|
|
348
|
+
"timestamp": 4027,
|
|
349
|
+
"cursorType": "pointer",
|
|
350
|
+
"type": "move"
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
"x": 2002,
|
|
354
|
+
"y": 1151,
|
|
355
|
+
"timestamp": 4049,
|
|
356
|
+
"cursorType": "pointer",
|
|
357
|
+
"type": "move"
|
|
92
358
|
}
|
|
93
359
|
]
|
package/cursor-test.js
CHANGED
|
@@ -1,50 +1,22 @@
|
|
|
1
|
-
const MacRecorder = require("./index");
|
|
2
|
-
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
console.log("\n");
|
|
24
|
-
|
|
25
|
-
// Durdur
|
|
26
|
-
await recorder.stopCursorCapture();
|
|
27
|
-
console.log("ā
Kayıt tamamlandı!");
|
|
28
|
-
|
|
29
|
-
// SonuƧ
|
|
30
|
-
if (fs.existsSync(outputPath)) {
|
|
31
|
-
const data = JSON.parse(fs.readFileSync(outputPath, "utf8"));
|
|
32
|
-
console.log(`š ${data.length} event kaydedildi -> ${outputPath}`);
|
|
33
|
-
|
|
34
|
-
// Basit istatistik
|
|
35
|
-
const clicks = data.filter((d) => d.type === "mousedown").length;
|
|
36
|
-
if (clicks > 0) {
|
|
37
|
-
console.log(`š±ļø ${clicks} click algılandı`);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
} catch (error) {
|
|
41
|
-
console.error("ā Hata:", error.message);
|
|
42
|
-
}
|
|
1
|
+
const MacRecorder = require("./index.js");
|
|
2
|
+
|
|
3
|
+
const recorder = new MacRecorder();
|
|
4
|
+
|
|
5
|
+
console.log("Starting cursor tracking test...");
|
|
6
|
+
console.log(
|
|
7
|
+
"Move your cursor around different applications to test cursor type detection"
|
|
8
|
+
);
|
|
9
|
+
console.log("The test will run for 10 seconds");
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
recorder.startCursorCapture("./cursor-data.json");
|
|
13
|
+
|
|
14
|
+
setTimeout(() => {
|
|
15
|
+
recorder.stopCursorCapture();
|
|
16
|
+
console.log("Test completed. Check cursor-data.json for results");
|
|
17
|
+
process.exit(0);
|
|
18
|
+
}, 10000);
|
|
19
|
+
} catch (error) {
|
|
20
|
+
console.error("Error during cursor tracking:", error);
|
|
21
|
+
process.exit(1);
|
|
43
22
|
}
|
|
44
|
-
|
|
45
|
-
// Direkt ƧalıÅtır
|
|
46
|
-
if (require.main === module) {
|
|
47
|
-
testCursorCapture().catch(console.error);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
module.exports = { testCursorCapture };
|
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
|
/**
|
|
@@ -404,8 +390,16 @@ class MacRecorder extends EventEmitter {
|
|
|
404
390
|
|
|
405
391
|
return new Promise((resolve, reject) => {
|
|
406
392
|
try {
|
|
393
|
+
// Get all displays first to validate the ID
|
|
394
|
+
const displays = nativeBinding.getDisplays();
|
|
395
|
+
const display = displays.find((d) => d.id === displayId);
|
|
396
|
+
|
|
397
|
+
if (!display) {
|
|
398
|
+
throw new Error(`Display with ID ${displayId} not found`);
|
|
399
|
+
}
|
|
400
|
+
|
|
407
401
|
const base64Image = nativeBinding.getDisplayThumbnail(
|
|
408
|
-
|
|
402
|
+
display.id, // Use the actual CGDirectDisplayID
|
|
409
403
|
maxWidth,
|
|
410
404
|
maxHeight
|
|
411
405
|
);
|
|
@@ -586,6 +580,52 @@ class MacRecorder extends EventEmitter {
|
|
|
586
580
|
nativeModule: "mac_recorder.node",
|
|
587
581
|
};
|
|
588
582
|
}
|
|
583
|
+
|
|
584
|
+
async getDisplaysWithThumbnails(options = {}) {
|
|
585
|
+
const displays = await this.getDisplays();
|
|
586
|
+
|
|
587
|
+
// Get thumbnails for each display
|
|
588
|
+
const displayPromises = displays.map(async (display) => {
|
|
589
|
+
try {
|
|
590
|
+
const thumbnail = await this.getDisplayThumbnail(display.id, options);
|
|
591
|
+
return {
|
|
592
|
+
...display,
|
|
593
|
+
thumbnail,
|
|
594
|
+
};
|
|
595
|
+
} catch (error) {
|
|
596
|
+
return {
|
|
597
|
+
...display,
|
|
598
|
+
thumbnail: null,
|
|
599
|
+
thumbnailError: error.message,
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
return Promise.all(displayPromises);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
async getWindowsWithThumbnails(options = {}) {
|
|
608
|
+
const windows = await this.getWindows();
|
|
609
|
+
|
|
610
|
+
// Get thumbnails for each window
|
|
611
|
+
const windowPromises = windows.map(async (window) => {
|
|
612
|
+
try {
|
|
613
|
+
const thumbnail = await this.getWindowThumbnail(window.id, options);
|
|
614
|
+
return {
|
|
615
|
+
...window,
|
|
616
|
+
thumbnail,
|
|
617
|
+
};
|
|
618
|
+
} catch (error) {
|
|
619
|
+
return {
|
|
620
|
+
...window,
|
|
621
|
+
thumbnail: null,
|
|
622
|
+
thumbnailError: error.message,
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
return Promise.all(windowPromises);
|
|
628
|
+
}
|
|
589
629
|
}
|
|
590
630
|
|
|
591
631
|
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/cursor_tracker.mm
CHANGED
|
@@ -43,47 +43,113 @@ static bool g_leftMouseDown = false;
|
|
|
43
43
|
static bool g_rightMouseDown = false;
|
|
44
44
|
static NSString *g_lastEventType = @"move";
|
|
45
45
|
|
|
46
|
-
//
|
|
46
|
+
// Event tap callback
|
|
47
|
+
static CGEventRef eventTapCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *userInfo) {
|
|
48
|
+
return event;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Cursor type detection helper - sistem genelindeki cursor type'ı al
|
|
47
52
|
NSString* getCursorType() {
|
|
48
53
|
@autoreleasepool {
|
|
49
54
|
g_cursorTypeCounter++;
|
|
50
55
|
|
|
51
56
|
@try {
|
|
52
|
-
//
|
|
53
|
-
NSCursor *currentCursor = [NSCursor
|
|
57
|
+
// Get current cursor info
|
|
58
|
+
NSCursor *currentCursor = [NSCursor currentSystemCursor];
|
|
59
|
+
NSString *cursorType = @"default";
|
|
60
|
+
|
|
61
|
+
// Get cursor image info
|
|
62
|
+
NSImage *cursorImage = [currentCursor image];
|
|
63
|
+
NSPoint hotSpot = [currentCursor hotSpot];
|
|
64
|
+
NSSize imageSize = [cursorImage size];
|
|
54
65
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
g_lastDetectedCursorType = @"pointer";
|
|
66
|
+
// Check cursor type by comparing with standard cursors
|
|
67
|
+
if ([currentCursor isEqual:[NSCursor pointingHandCursor]] ||
|
|
68
|
+
(hotSpot.x >= 5 && hotSpot.x <= 7 && hotSpot.y >= 0 && hotSpot.y <= 4) ||
|
|
69
|
+
(hotSpot.x >= 12 && hotSpot.x <= 14 && hotSpot.y >= 7 && hotSpot.y <= 9)) {
|
|
60
70
|
return @"pointer";
|
|
61
|
-
} else if (currentCursor
|
|
62
|
-
|
|
71
|
+
} else if ([currentCursor isEqual:[NSCursor IBeamCursor]] ||
|
|
72
|
+
(hotSpot.x >= 3 && hotSpot.x <= 5 && hotSpot.y >= 8 && hotSpot.y <= 10 &&
|
|
73
|
+
imageSize.width <= 10 && imageSize.height >= 16)) {
|
|
63
74
|
return @"text";
|
|
64
|
-
} else if (currentCursor
|
|
65
|
-
g_lastDetectedCursorType = @"grab";
|
|
66
|
-
return @"grab";
|
|
67
|
-
} else if (currentCursor == [NSCursor closedHandCursor]) {
|
|
68
|
-
g_lastDetectedCursorType = @"grabbing";
|
|
69
|
-
return @"grabbing";
|
|
70
|
-
} else if (currentCursor == [NSCursor resizeLeftRightCursor]) {
|
|
71
|
-
g_lastDetectedCursorType = @"ew-resize";
|
|
75
|
+
} else if ([currentCursor isEqual:[NSCursor resizeLeftRightCursor]]) {
|
|
72
76
|
return @"ew-resize";
|
|
73
|
-
} else if (currentCursor
|
|
74
|
-
g_lastDetectedCursorType = @"ns-resize";
|
|
77
|
+
} else if ([currentCursor isEqual:[NSCursor resizeUpDownCursor]]) {
|
|
75
78
|
return @"ns-resize";
|
|
76
|
-
} else if (currentCursor
|
|
77
|
-
|
|
78
|
-
return @"
|
|
79
|
-
} else {
|
|
80
|
-
// Bilinmeyen cursor - default olarak dƶn
|
|
81
|
-
g_lastDetectedCursorType = @"default";
|
|
82
|
-
return @"default";
|
|
79
|
+
} else if ([currentCursor isEqual:[NSCursor openHandCursor]] ||
|
|
80
|
+
[currentCursor isEqual:[NSCursor closedHandCursor]]) {
|
|
81
|
+
return @"grabbing";
|
|
83
82
|
}
|
|
83
|
+
|
|
84
|
+
// Check if we're in a drag operation
|
|
85
|
+
CGEventRef event = CGEventCreate(NULL);
|
|
86
|
+
if (event) {
|
|
87
|
+
CGEventType eventType = (CGEventType)CGEventGetType(event);
|
|
88
|
+
if (eventType == kCGEventLeftMouseDragged ||
|
|
89
|
+
eventType == kCGEventRightMouseDragged) {
|
|
90
|
+
CFRelease(event);
|
|
91
|
+
return @"grabbing";
|
|
92
|
+
}
|
|
93
|
+
CFRelease(event);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Get the window under the cursor
|
|
97
|
+
CGPoint cursorPos = CGEventGetLocation(CGEventCreate(NULL));
|
|
98
|
+
AXUIElementRef systemWide = AXUIElementCreateSystemWide();
|
|
99
|
+
AXUIElementRef elementAtPosition = NULL;
|
|
100
|
+
AXError error = AXUIElementCopyElementAtPosition(systemWide, cursorPos.x, cursorPos.y, &elementAtPosition);
|
|
101
|
+
|
|
102
|
+
if (error == kAXErrorSuccess && elementAtPosition) {
|
|
103
|
+
CFStringRef role = NULL;
|
|
104
|
+
error = AXUIElementCopyAttributeValue(elementAtPosition, kAXRoleAttribute, (CFTypeRef*)&role);
|
|
105
|
+
|
|
106
|
+
if (error == kAXErrorSuccess && role) {
|
|
107
|
+
NSString *elementRole = (__bridge_transfer NSString*)role;
|
|
108
|
+
|
|
109
|
+
// Check for clickable elements that should show pointer cursor
|
|
110
|
+
if ([elementRole isEqualToString:@"AXLink"] ||
|
|
111
|
+
[elementRole isEqualToString:@"AXButton"] ||
|
|
112
|
+
[elementRole isEqualToString:@"AXMenuItem"] ||
|
|
113
|
+
[elementRole isEqualToString:@"AXRadioButton"] ||
|
|
114
|
+
[elementRole isEqualToString:@"AXCheckBox"]) {
|
|
115
|
+
return @"pointer";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Check subrole for additional pointer cursor elements
|
|
119
|
+
CFStringRef subrole = NULL;
|
|
120
|
+
error = AXUIElementCopyAttributeValue(elementAtPosition, kAXSubroleAttribute, (CFTypeRef*)&subrole);
|
|
121
|
+
if (error == kAXErrorSuccess && subrole) {
|
|
122
|
+
NSString *elementSubrole = (__bridge_transfer NSString*)subrole;
|
|
123
|
+
|
|
124
|
+
if ([elementSubrole isEqualToString:@"AXClickable"] ||
|
|
125
|
+
[elementSubrole isEqualToString:@"AXDisclosureTriangle"] ||
|
|
126
|
+
[elementSubrole isEqualToString:@"AXToolbarButton"] ||
|
|
127
|
+
[elementSubrole isEqualToString:@"AXCloseButton"] ||
|
|
128
|
+
[elementSubrole isEqualToString:@"AXMinimizeButton"] ||
|
|
129
|
+
[elementSubrole isEqualToString:@"AXZoomButton"]) {
|
|
130
|
+
return @"pointer";
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Check for text elements
|
|
135
|
+
if ([elementRole isEqualToString:@"AXTextField"] ||
|
|
136
|
+
[elementRole isEqualToString:@"AXTextArea"] ||
|
|
137
|
+
[elementRole isEqualToString:@"AXStaticText"]) {
|
|
138
|
+
return @"text";
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
CFRelease(elementAtPosition);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (systemWide) {
|
|
146
|
+
CFRelease(systemWide);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return cursorType;
|
|
150
|
+
|
|
84
151
|
} @catch (NSException *exception) {
|
|
85
|
-
|
|
86
|
-
g_lastDetectedCursorType = @"default";
|
|
152
|
+
NSLog(@"Error in getCursorType: %@", exception);
|
|
87
153
|
return @"default";
|
|
88
154
|
}
|
|
89
155
|
}
|
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
|
|
|
@@ -363,35 +366,20 @@ Napi::Value GetDisplays(const Napi::CallbackInfo& info) {
|
|
|
363
366
|
Napi::Env env = info.Env();
|
|
364
367
|
|
|
365
368
|
@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
|
-
}
|
|
369
|
+
NSArray *displays = [ScreenCapture getAvailableDisplays];
|
|
370
|
+
Napi::Array result = Napi::Array::New(env, displays.count);
|
|
388
371
|
|
|
389
|
-
|
|
372
|
+
NSLog(@"Found %lu displays", (unsigned long)displays.count);
|
|
390
373
|
|
|
391
|
-
// Convert to NAPI array
|
|
392
|
-
Napi::Array result = Napi::Array::New(env, displays.count);
|
|
393
374
|
for (NSUInteger i = 0; i < displays.count; i++) {
|
|
394
375
|
NSDictionary *display = displays[i];
|
|
376
|
+
NSLog(@"Display %lu: ID=%u, Name=%@, Size=%@x%@",
|
|
377
|
+
(unsigned long)i,
|
|
378
|
+
[display[@"id"] unsignedIntValue],
|
|
379
|
+
display[@"name"],
|
|
380
|
+
display[@"width"],
|
|
381
|
+
display[@"height"]);
|
|
382
|
+
|
|
395
383
|
Napi::Object displayObj = Napi::Object::New(env);
|
|
396
384
|
displayObj.Set("id", Napi::Number::New(env, [display[@"id"] unsignedIntValue]));
|
|
397
385
|
displayObj.Set("name", Napi::String::New(env, [display[@"name"] UTF8String]));
|
|
@@ -406,6 +394,7 @@ Napi::Value GetDisplays(const Napi::CallbackInfo& info) {
|
|
|
406
394
|
return result;
|
|
407
395
|
|
|
408
396
|
} @catch (NSException *exception) {
|
|
397
|
+
NSLog(@"Exception in GetDisplays: %@", exception);
|
|
409
398
|
return Napi::Array::New(env, 0);
|
|
410
399
|
}
|
|
411
400
|
}
|
|
@@ -536,10 +525,34 @@ Napi::Value GetDisplayThumbnail(const Napi::CallbackInfo& info) {
|
|
|
536
525
|
}
|
|
537
526
|
|
|
538
527
|
@try {
|
|
528
|
+
// Verify display exists
|
|
529
|
+
CGDirectDisplayID activeDisplays[32];
|
|
530
|
+
uint32_t displayCount;
|
|
531
|
+
CGError err = CGGetActiveDisplayList(32, activeDisplays, &displayCount);
|
|
532
|
+
|
|
533
|
+
if (err != kCGErrorSuccess) {
|
|
534
|
+
NSLog(@"Failed to get active display list: %d", err);
|
|
535
|
+
return env.Null();
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
bool displayFound = false;
|
|
539
|
+
for (uint32_t i = 0; i < displayCount; i++) {
|
|
540
|
+
if (activeDisplays[i] == displayID) {
|
|
541
|
+
displayFound = true;
|
|
542
|
+
break;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (!displayFound) {
|
|
547
|
+
NSLog(@"Display ID %u not found in active displays", displayID);
|
|
548
|
+
return env.Null();
|
|
549
|
+
}
|
|
550
|
+
|
|
539
551
|
// Create display image
|
|
540
552
|
CGImageRef displayImage = CGDisplayCreateImage(displayID);
|
|
541
553
|
|
|
542
554
|
if (!displayImage) {
|
|
555
|
+
NSLog(@"CGDisplayCreateImage failed for display ID: %u", displayID);
|
|
543
556
|
return env.Null();
|
|
544
557
|
}
|
|
545
558
|
|
|
@@ -547,6 +560,8 @@ Napi::Value GetDisplayThumbnail(const Napi::CallbackInfo& info) {
|
|
|
547
560
|
size_t originalWidth = CGImageGetWidth(displayImage);
|
|
548
561
|
size_t originalHeight = CGImageGetHeight(displayImage);
|
|
549
562
|
|
|
563
|
+
NSLog(@"Original dimensions: %zux%zu", originalWidth, originalHeight);
|
|
564
|
+
|
|
550
565
|
// Calculate scaled dimensions maintaining aspect ratio
|
|
551
566
|
double scaleX = (double)maxWidth / originalWidth;
|
|
552
567
|
double scaleY = (double)maxHeight / originalHeight;
|
|
@@ -555,6 +570,8 @@ Napi::Value GetDisplayThumbnail(const Napi::CallbackInfo& info) {
|
|
|
555
570
|
size_t thumbnailWidth = (size_t)(originalWidth * scale);
|
|
556
571
|
size_t thumbnailHeight = (size_t)(originalHeight * scale);
|
|
557
572
|
|
|
573
|
+
NSLog(@"Thumbnail dimensions: %zux%zu (scale: %f)", thumbnailWidth, thumbnailHeight, scale);
|
|
574
|
+
|
|
558
575
|
// Create scaled image
|
|
559
576
|
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
|
|
560
577
|
CGContextRef context = CGBitmapContextCreate(
|
|
@@ -564,43 +581,61 @@ Napi::Value GetDisplayThumbnail(const Napi::CallbackInfo& info) {
|
|
|
564
581
|
8,
|
|
565
582
|
thumbnailWidth * 4,
|
|
566
583
|
colorSpace,
|
|
567
|
-
kCGImageAlphaPremultipliedLast
|
|
584
|
+
kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big
|
|
568
585
|
);
|
|
569
586
|
|
|
570
|
-
if (context) {
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
587
|
+
if (!context) {
|
|
588
|
+
NSLog(@"Failed to create bitmap context");
|
|
589
|
+
CGImageRelease(displayImage);
|
|
590
|
+
CGColorSpaceRelease(colorSpace);
|
|
591
|
+
return env.Null();
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Set interpolation quality for better scaling
|
|
595
|
+
CGContextSetInterpolationQuality(context, kCGInterpolationHigh);
|
|
596
|
+
|
|
597
|
+
// Draw the image
|
|
598
|
+
CGContextDrawImage(context, CGRectMake(0, 0, thumbnailWidth, thumbnailHeight), displayImage);
|
|
599
|
+
CGImageRef thumbnailImage = CGBitmapContextCreateImage(context);
|
|
600
|
+
|
|
601
|
+
if (!thumbnailImage) {
|
|
602
|
+
NSLog(@"Failed to create thumbnail image");
|
|
603
|
+
CGContextRelease(context);
|
|
604
|
+
CGImageRelease(displayImage);
|
|
605
|
+
CGColorSpaceRelease(colorSpace);
|
|
606
|
+
return env.Null();
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Convert to PNG data
|
|
610
|
+
NSBitmapImageRep *imageRep = [[NSBitmapImageRep alloc] initWithCGImage:thumbnailImage];
|
|
611
|
+
NSDictionary *properties = @{NSImageCompressionFactor: @0.8};
|
|
612
|
+
NSData *pngData = [imageRep representationUsingType:NSBitmapImageFileTypePNG properties:properties];
|
|
613
|
+
|
|
614
|
+
if (!pngData) {
|
|
615
|
+
NSLog(@"Failed to convert image to PNG data");
|
|
616
|
+
CGImageRelease(thumbnailImage);
|
|
595
617
|
CGContextRelease(context);
|
|
618
|
+
CGImageRelease(displayImage);
|
|
619
|
+
CGColorSpaceRelease(colorSpace);
|
|
620
|
+
return env.Null();
|
|
596
621
|
}
|
|
597
622
|
|
|
623
|
+
// Convert to Base64
|
|
624
|
+
NSString *base64String = [pngData base64EncodedStringWithOptions:0];
|
|
625
|
+
std::string base64Std = [base64String UTF8String];
|
|
626
|
+
|
|
627
|
+
NSLog(@"Successfully created thumbnail with base64 length: %lu", (unsigned long)base64Std.length());
|
|
628
|
+
|
|
629
|
+
// Cleanup
|
|
630
|
+
CGImageRelease(thumbnailImage);
|
|
631
|
+
CGContextRelease(context);
|
|
598
632
|
CGColorSpaceRelease(colorSpace);
|
|
599
633
|
CGImageRelease(displayImage);
|
|
600
634
|
|
|
601
|
-
return env
|
|
635
|
+
return Napi::String::New(env, base64Std);
|
|
602
636
|
|
|
603
637
|
} @catch (NSException *exception) {
|
|
638
|
+
NSLog(@"Exception in GetDisplayThumbnail: %@", exception);
|
|
604
639
|
return env.Null();
|
|
605
640
|
}
|
|
606
641
|
}
|
|
@@ -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
|