node-mac-recorder 1.0.5 → 1.1.0
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 +93 -0
- package/index.js +58 -0
- package/package.json +1 -1
- package/preview-test.js +329 -0
- package/src/mac_recorder.mm +193 -0
package/README.md
CHANGED
|
@@ -25,6 +25,7 @@ A powerful native macOS screen recording Node.js package with advanced window se
|
|
|
25
25
|
- 🎯 **Automatic Coordinate Conversion** - Handle multi-display coordinate systems
|
|
26
26
|
- 📐 **Display ID Detection** - Automatically select correct display for window recording
|
|
27
27
|
- 🖼️ **Window Filtering** - Smart filtering of recordable windows
|
|
28
|
+
- 👁️ **Preview Thumbnails** - Generate window and display preview images
|
|
28
29
|
|
|
29
30
|
⚙️ **Customization Options**
|
|
30
31
|
|
|
@@ -202,6 +203,34 @@ console.log(status);
|
|
|
202
203
|
// }
|
|
203
204
|
```
|
|
204
205
|
|
|
206
|
+
#### `getWindowThumbnail(windowId, options?)`
|
|
207
|
+
|
|
208
|
+
Captures a thumbnail preview of a specific window.
|
|
209
|
+
|
|
210
|
+
```javascript
|
|
211
|
+
const thumbnail = await recorder.getWindowThumbnail(12345, {
|
|
212
|
+
maxWidth: 400, // Maximum width (default: 300)
|
|
213
|
+
maxHeight: 300, // Maximum height (default: 200)
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// Returns: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..."
|
|
217
|
+
// Can be used directly in <img> tags or saved as file
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
#### `getDisplayThumbnail(displayId, options?)`
|
|
221
|
+
|
|
222
|
+
Captures a thumbnail preview of a specific display.
|
|
223
|
+
|
|
224
|
+
```javascript
|
|
225
|
+
const thumbnail = await recorder.getDisplayThumbnail(0, {
|
|
226
|
+
maxWidth: 400, // Maximum width (default: 300)
|
|
227
|
+
maxHeight: 300, // Maximum height (default: 200)
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Returns: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..."
|
|
231
|
+
// Perfect for display selection UI
|
|
232
|
+
```
|
|
233
|
+
|
|
205
234
|
## Usage Examples
|
|
206
235
|
|
|
207
236
|
### Window-Specific Recording
|
|
@@ -311,6 +340,70 @@ recorder.on("completed", (outputPath) => {
|
|
|
311
340
|
await recorder.startRecording("./event-recording.mov");
|
|
312
341
|
```
|
|
313
342
|
|
|
343
|
+
### Window Selection with Thumbnails
|
|
344
|
+
|
|
345
|
+
```javascript
|
|
346
|
+
const recorder = new MacRecorder();
|
|
347
|
+
|
|
348
|
+
// Get windows with thumbnail previews
|
|
349
|
+
const windows = await recorder.getWindows();
|
|
350
|
+
|
|
351
|
+
console.log("Available windows with previews:");
|
|
352
|
+
for (const window of windows) {
|
|
353
|
+
console.log(`${window.appName} - ${window.name}`);
|
|
354
|
+
|
|
355
|
+
try {
|
|
356
|
+
// Generate thumbnail for each window
|
|
357
|
+
const thumbnail = await recorder.getWindowThumbnail(window.id, {
|
|
358
|
+
maxWidth: 200,
|
|
359
|
+
maxHeight: 150,
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
console.log(`Thumbnail: ${thumbnail.substring(0, 50)}...`);
|
|
363
|
+
|
|
364
|
+
// Use thumbnail in your UI:
|
|
365
|
+
// <img src="${thumbnail}" alt="Window Preview" />
|
|
366
|
+
} catch (error) {
|
|
367
|
+
console.log(`No preview available: ${error.message}`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### Display Selection Interface
|
|
373
|
+
|
|
374
|
+
```javascript
|
|
375
|
+
const recorder = new MacRecorder();
|
|
376
|
+
|
|
377
|
+
async function createDisplaySelector() {
|
|
378
|
+
const displays = await recorder.getDisplays();
|
|
379
|
+
|
|
380
|
+
const displayOptions = await Promise.all(
|
|
381
|
+
displays.map(async (display, index) => {
|
|
382
|
+
try {
|
|
383
|
+
const thumbnail = await recorder.getDisplayThumbnail(display.id);
|
|
384
|
+
return {
|
|
385
|
+
id: display.id,
|
|
386
|
+
name: `Display ${index + 1}`,
|
|
387
|
+
resolution: display.resolution,
|
|
388
|
+
thumbnail: thumbnail,
|
|
389
|
+
isPrimary: display.isPrimary,
|
|
390
|
+
};
|
|
391
|
+
} catch (error) {
|
|
392
|
+
return {
|
|
393
|
+
id: display.id,
|
|
394
|
+
name: `Display ${index + 1}`,
|
|
395
|
+
resolution: display.resolution,
|
|
396
|
+
thumbnail: null,
|
|
397
|
+
isPrimary: display.isPrimary,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
})
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
return displayOptions;
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
314
407
|
## Integration Examples
|
|
315
408
|
|
|
316
409
|
### Electron Integration
|
package/index.js
CHANGED
|
@@ -355,6 +355,64 @@ class MacRecorder extends EventEmitter {
|
|
|
355
355
|
});
|
|
356
356
|
}
|
|
357
357
|
|
|
358
|
+
/**
|
|
359
|
+
* Pencere önizleme görüntüsü alır (Base64 PNG)
|
|
360
|
+
*/
|
|
361
|
+
async getWindowThumbnail(windowId, options = {}) {
|
|
362
|
+
if (!windowId) {
|
|
363
|
+
throw new Error("Window ID is required");
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const { maxWidth = 300, maxHeight = 200 } = options;
|
|
367
|
+
|
|
368
|
+
return new Promise((resolve, reject) => {
|
|
369
|
+
try {
|
|
370
|
+
const base64Image = nativeBinding.getWindowThumbnail(
|
|
371
|
+
windowId,
|
|
372
|
+
maxWidth,
|
|
373
|
+
maxHeight
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
if (base64Image) {
|
|
377
|
+
resolve(`data:image/png;base64,${base64Image}`);
|
|
378
|
+
} else {
|
|
379
|
+
reject(new Error("Failed to capture window thumbnail"));
|
|
380
|
+
}
|
|
381
|
+
} catch (error) {
|
|
382
|
+
reject(error);
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Ekran önizleme görüntüsü alır (Base64 PNG)
|
|
389
|
+
*/
|
|
390
|
+
async getDisplayThumbnail(displayId, options = {}) {
|
|
391
|
+
if (displayId === null || displayId === undefined) {
|
|
392
|
+
throw new Error("Display ID is required");
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const { maxWidth = 300, maxHeight = 200 } = options;
|
|
396
|
+
|
|
397
|
+
return new Promise((resolve, reject) => {
|
|
398
|
+
try {
|
|
399
|
+
const base64Image = nativeBinding.getDisplayThumbnail(
|
|
400
|
+
displayId,
|
|
401
|
+
maxWidth,
|
|
402
|
+
maxHeight
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
if (base64Image) {
|
|
406
|
+
resolve(`data:image/png;base64,${base64Image}`);
|
|
407
|
+
} else {
|
|
408
|
+
reject(new Error("Failed to capture display thumbnail"));
|
|
409
|
+
}
|
|
410
|
+
} catch (error) {
|
|
411
|
+
reject(error);
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
358
416
|
/**
|
|
359
417
|
* Native modül bilgilerini döndürür
|
|
360
418
|
*/
|
package/package.json
CHANGED
package/preview-test.js
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
const MacRecorder = require("./index.js");
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
|
|
4
|
+
async function testPreviews() {
|
|
5
|
+
const recorder = new MacRecorder();
|
|
6
|
+
|
|
7
|
+
console.log("🖼️ Thumbnail Preview Test\n");
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
// 1. Display Previews
|
|
11
|
+
console.log("📺 Display Thumbnails Test...");
|
|
12
|
+
const displays = await recorder.getDisplays();
|
|
13
|
+
|
|
14
|
+
console.log(`Found ${displays.length} displays:`);
|
|
15
|
+
for (let i = 0; i < displays.length; i++) {
|
|
16
|
+
const display = displays[i];
|
|
17
|
+
console.log(
|
|
18
|
+
` Display ${i}: ${display.resolution} ${
|
|
19
|
+
display.isPrimary ? "(Primary)" : ""
|
|
20
|
+
}`
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
console.log(` 📸 Capturing thumbnail...`);
|
|
25
|
+
const thumbnail = await recorder.getDisplayThumbnail(display.id, {
|
|
26
|
+
maxWidth: 300,
|
|
27
|
+
maxHeight: 200,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
console.log(` ✅ Success: ${thumbnail.length} chars`);
|
|
31
|
+
|
|
32
|
+
// Save as HTML file to view
|
|
33
|
+
const htmlContent = `
|
|
34
|
+
<!DOCTYPE html>
|
|
35
|
+
<html>
|
|
36
|
+
<head>
|
|
37
|
+
<title>Display ${i} Preview</title>
|
|
38
|
+
<style>
|
|
39
|
+
body {
|
|
40
|
+
font-family: Arial, sans-serif;
|
|
41
|
+
text-align: center;
|
|
42
|
+
padding: 20px;
|
|
43
|
+
background: #f5f5f5;
|
|
44
|
+
}
|
|
45
|
+
.preview-card {
|
|
46
|
+
background: white;
|
|
47
|
+
padding: 20px;
|
|
48
|
+
border-radius: 12px;
|
|
49
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
|
50
|
+
max-width: 400px;
|
|
51
|
+
margin: 0 auto;
|
|
52
|
+
}
|
|
53
|
+
img {
|
|
54
|
+
border: 2px solid #007AFF;
|
|
55
|
+
border-radius: 8px;
|
|
56
|
+
max-width: 100%;
|
|
57
|
+
}
|
|
58
|
+
.info {
|
|
59
|
+
background: #f0f0f0;
|
|
60
|
+
padding: 15px;
|
|
61
|
+
border-radius: 8px;
|
|
62
|
+
margin: 15px 0;
|
|
63
|
+
text-align: left;
|
|
64
|
+
}
|
|
65
|
+
h1 { color: #007AFF; }
|
|
66
|
+
</style>
|
|
67
|
+
</head>
|
|
68
|
+
<body>
|
|
69
|
+
<div class="preview-card">
|
|
70
|
+
<h1>📺 Display ${i} Preview</h1>
|
|
71
|
+
<div class="info">
|
|
72
|
+
<strong>Resolution:</strong> ${display.resolution}<br>
|
|
73
|
+
<strong>Position:</strong> (${display.x}, ${display.y})<br>
|
|
74
|
+
<strong>Primary:</strong> ${display.isPrimary ? "Yes" : "No"}<br>
|
|
75
|
+
<strong>Display ID:</strong> ${display.id}
|
|
76
|
+
</div>
|
|
77
|
+
<img src="${thumbnail}" alt="Display ${i} Preview" />
|
|
78
|
+
<p><small>Captured: ${new Date().toLocaleString()}</small></p>
|
|
79
|
+
</div>
|
|
80
|
+
</body>
|
|
81
|
+
</html>`;
|
|
82
|
+
|
|
83
|
+
fs.writeFileSync(`display-${i}-preview.html`, htmlContent);
|
|
84
|
+
console.log(` 💾 Saved: display-${i}-preview.html\n`);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.log(` ❌ Failed: ${error.message}\n`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 2. Window Previews
|
|
91
|
+
console.log("🪟 Window Thumbnails Test...");
|
|
92
|
+
const windows = await recorder.getWindows();
|
|
93
|
+
|
|
94
|
+
// Test first 3 windows
|
|
95
|
+
const testWindows = windows.slice(0, 3);
|
|
96
|
+
console.log(`Testing ${testWindows.length} windows:`);
|
|
97
|
+
|
|
98
|
+
for (let i = 0; i < testWindows.length; i++) {
|
|
99
|
+
const window = testWindows[i];
|
|
100
|
+
console.log(` Window ${i}: [${window.appName}] ${window.name}`);
|
|
101
|
+
console.log(` Size: ${window.width}x${window.height}`);
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
console.log(` 📸 Capturing thumbnail...`);
|
|
105
|
+
const thumbnail = await recorder.getWindowThumbnail(window.id, {
|
|
106
|
+
maxWidth: 300,
|
|
107
|
+
maxHeight: 200,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
console.log(` ✅ Success: ${thumbnail.length} chars`);
|
|
111
|
+
|
|
112
|
+
// Save as HTML file to view
|
|
113
|
+
const htmlContent = `
|
|
114
|
+
<!DOCTYPE html>
|
|
115
|
+
<html>
|
|
116
|
+
<head>
|
|
117
|
+
<title>${window.appName} Preview</title>
|
|
118
|
+
<style>
|
|
119
|
+
body {
|
|
120
|
+
font-family: Arial, sans-serif;
|
|
121
|
+
text-align: center;
|
|
122
|
+
padding: 20px;
|
|
123
|
+
background: #f5f5f5;
|
|
124
|
+
}
|
|
125
|
+
.preview-card {
|
|
126
|
+
background: white;
|
|
127
|
+
padding: 20px;
|
|
128
|
+
border-radius: 12px;
|
|
129
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
|
130
|
+
max-width: 400px;
|
|
131
|
+
margin: 0 auto;
|
|
132
|
+
}
|
|
133
|
+
img {
|
|
134
|
+
border: 2px solid #FF3B30;
|
|
135
|
+
border-radius: 8px;
|
|
136
|
+
max-width: 100%;
|
|
137
|
+
}
|
|
138
|
+
.info {
|
|
139
|
+
background: #f0f0f0;
|
|
140
|
+
padding: 15px;
|
|
141
|
+
border-radius: 8px;
|
|
142
|
+
margin: 15px 0;
|
|
143
|
+
text-align: left;
|
|
144
|
+
}
|
|
145
|
+
h1 { color: #FF3B30; }
|
|
146
|
+
.app-name { color: #007AFF; font-weight: bold; }
|
|
147
|
+
</style>
|
|
148
|
+
</head>
|
|
149
|
+
<body>
|
|
150
|
+
<div class="preview-card">
|
|
151
|
+
<h1>🪟 <span class="app-name">${window.appName}</span></h1>
|
|
152
|
+
<div class="info">
|
|
153
|
+
<strong>Window:</strong> ${window.name}<br>
|
|
154
|
+
<strong>Size:</strong> ${window.width}x${window.height}<br>
|
|
155
|
+
<strong>Position:</strong> (${window.x}, ${window.y})<br>
|
|
156
|
+
<strong>Window ID:</strong> ${window.id}
|
|
157
|
+
</div>
|
|
158
|
+
<img src="${thumbnail}" alt="${window.appName} Preview" />
|
|
159
|
+
<p><small>Captured: ${new Date().toLocaleString()}</small></p>
|
|
160
|
+
</div>
|
|
161
|
+
</body>
|
|
162
|
+
</html>`;
|
|
163
|
+
|
|
164
|
+
const fileName = `window-${window.appName.replace(
|
|
165
|
+
/[^a-zA-Z0-9]/g,
|
|
166
|
+
""
|
|
167
|
+
)}-${i}-preview.html`;
|
|
168
|
+
fs.writeFileSync(fileName, htmlContent);
|
|
169
|
+
console.log(` 💾 Saved: ${fileName}\n`);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
console.log(` ❌ Failed: ${error.message}\n`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 3. Create Gallery
|
|
176
|
+
console.log("🎨 Creating Preview Gallery...");
|
|
177
|
+
const previewFiles = fs
|
|
178
|
+
.readdirSync(".")
|
|
179
|
+
.filter((file) => file.endsWith("-preview.html"))
|
|
180
|
+
.sort();
|
|
181
|
+
|
|
182
|
+
if (previewFiles.length > 0) {
|
|
183
|
+
const galleryContent = `
|
|
184
|
+
<!DOCTYPE html>
|
|
185
|
+
<html>
|
|
186
|
+
<head>
|
|
187
|
+
<title>📸 Thumbnail Preview Gallery</title>
|
|
188
|
+
<style>
|
|
189
|
+
body {
|
|
190
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
191
|
+
padding: 20px;
|
|
192
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
193
|
+
color: white;
|
|
194
|
+
min-height: 100vh;
|
|
195
|
+
}
|
|
196
|
+
.header {
|
|
197
|
+
text-align: center;
|
|
198
|
+
margin-bottom: 30px;
|
|
199
|
+
}
|
|
200
|
+
.gallery {
|
|
201
|
+
display: grid;
|
|
202
|
+
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
|
203
|
+
gap: 20px;
|
|
204
|
+
margin-top: 20px;
|
|
205
|
+
}
|
|
206
|
+
.preview-card {
|
|
207
|
+
background: rgba(255,255,255,0.1);
|
|
208
|
+
border-radius: 16px;
|
|
209
|
+
overflow: hidden;
|
|
210
|
+
backdrop-filter: blur(10px);
|
|
211
|
+
border: 1px solid rgba(255,255,255,0.2);
|
|
212
|
+
transition: transform 0.3s ease;
|
|
213
|
+
}
|
|
214
|
+
.preview-card:hover {
|
|
215
|
+
transform: translateY(-8px);
|
|
216
|
+
}
|
|
217
|
+
.card-header {
|
|
218
|
+
padding: 20px;
|
|
219
|
+
border-bottom: 1px solid rgba(255,255,255,0.1);
|
|
220
|
+
}
|
|
221
|
+
.card-title {
|
|
222
|
+
margin: 0;
|
|
223
|
+
font-size: 18px;
|
|
224
|
+
font-weight: 600;
|
|
225
|
+
}
|
|
226
|
+
.card-subtitle {
|
|
227
|
+
margin: 8px 0 0 0;
|
|
228
|
+
opacity: 0.8;
|
|
229
|
+
font-size: 14px;
|
|
230
|
+
}
|
|
231
|
+
iframe {
|
|
232
|
+
width: 100%;
|
|
233
|
+
height: 400px;
|
|
234
|
+
border: none;
|
|
235
|
+
background: white;
|
|
236
|
+
}
|
|
237
|
+
.stats {
|
|
238
|
+
background: rgba(255,255,255,0.2);
|
|
239
|
+
padding: 20px;
|
|
240
|
+
border-radius: 12px;
|
|
241
|
+
text-align: center;
|
|
242
|
+
margin-bottom: 30px;
|
|
243
|
+
backdrop-filter: blur(10px);
|
|
244
|
+
}
|
|
245
|
+
.btn {
|
|
246
|
+
background: rgba(255,255,255,0.2);
|
|
247
|
+
color: white;
|
|
248
|
+
padding: 10px 20px;
|
|
249
|
+
border: none;
|
|
250
|
+
border-radius: 8px;
|
|
251
|
+
cursor: pointer;
|
|
252
|
+
margin: 5px;
|
|
253
|
+
text-decoration: none;
|
|
254
|
+
display: inline-block;
|
|
255
|
+
transition: all 0.3s ease;
|
|
256
|
+
}
|
|
257
|
+
.btn:hover {
|
|
258
|
+
background: rgba(255,255,255,0.3);
|
|
259
|
+
transform: translateY(-2px);
|
|
260
|
+
}
|
|
261
|
+
</style>
|
|
262
|
+
</head>
|
|
263
|
+
<body>
|
|
264
|
+
<div class="header">
|
|
265
|
+
<h1>📸 macOS Thumbnail Preview Gallery</h1>
|
|
266
|
+
<p>Screen and window thumbnails generated with node-mac-recorder</p>
|
|
267
|
+
</div>
|
|
268
|
+
|
|
269
|
+
<div class="stats">
|
|
270
|
+
<strong>${previewFiles.length}</strong> Thumbnails Generated<br>
|
|
271
|
+
<small>Created at ${new Date().toLocaleString()}</small>
|
|
272
|
+
</div>
|
|
273
|
+
|
|
274
|
+
<div style="text-align: center; margin-bottom: 20px;">
|
|
275
|
+
${previewFiles
|
|
276
|
+
.map(
|
|
277
|
+
(file) =>
|
|
278
|
+
`<a href="${file}" class="btn" target="_blank">${file
|
|
279
|
+
.replace("-preview.html", "")
|
|
280
|
+
.replace(/-/g, " ")}</a>`
|
|
281
|
+
)
|
|
282
|
+
.join("")}
|
|
283
|
+
</div>
|
|
284
|
+
|
|
285
|
+
<div class="gallery">
|
|
286
|
+
${previewFiles
|
|
287
|
+
.map(
|
|
288
|
+
(file) => `
|
|
289
|
+
<div class="preview-card">
|
|
290
|
+
<div class="card-header">
|
|
291
|
+
<h3 class="card-title">${file
|
|
292
|
+
.replace("-preview.html", "")
|
|
293
|
+
.replace(/-/g, " ")
|
|
294
|
+
.toUpperCase()}</h3>
|
|
295
|
+
<p class="card-subtitle">${file}</p>
|
|
296
|
+
</div>
|
|
297
|
+
<iframe src="${file}"></iframe>
|
|
298
|
+
</div>
|
|
299
|
+
`
|
|
300
|
+
)
|
|
301
|
+
.join("")}
|
|
302
|
+
</div>
|
|
303
|
+
|
|
304
|
+
<div style="text-align: center; margin-top: 40px; opacity: 0.8;">
|
|
305
|
+
<p>🚀 Generated with <strong>node-mac-recorder v1.1.0</strong></p>
|
|
306
|
+
</div>
|
|
307
|
+
</body>
|
|
308
|
+
</html>`;
|
|
309
|
+
|
|
310
|
+
fs.writeFileSync("preview-gallery.html", galleryContent);
|
|
311
|
+
console.log(`✅ Gallery created: preview-gallery.html`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
console.log("\n🎉 Preview Test Completed!");
|
|
315
|
+
console.log("\n📁 Generated Files:");
|
|
316
|
+
previewFiles.forEach((file) => console.log(` - ${file}`));
|
|
317
|
+
console.log(" - preview-gallery.html (main gallery)");
|
|
318
|
+
|
|
319
|
+
console.log("\n🌐 View Results:");
|
|
320
|
+
console.log(" open preview-gallery.html");
|
|
321
|
+
console.log("\n💡 Individual files can be opened directly in browser!");
|
|
322
|
+
} catch (error) {
|
|
323
|
+
console.error("❌ Preview test failed:", error.message);
|
|
324
|
+
console.error(error.stack);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Run preview test
|
|
329
|
+
testPreviews();
|
package/src/mac_recorder.mm
CHANGED
|
@@ -413,6 +413,195 @@ Napi::Value GetRecordingStatus(const Napi::CallbackInfo& info) {
|
|
|
413
413
|
return Napi::Boolean::New(env, g_isRecording);
|
|
414
414
|
}
|
|
415
415
|
|
|
416
|
+
// NAPI Function: Get Window Thumbnail
|
|
417
|
+
Napi::Value GetWindowThumbnail(const Napi::CallbackInfo& info) {
|
|
418
|
+
Napi::Env env = info.Env();
|
|
419
|
+
|
|
420
|
+
if (info.Length() < 1) {
|
|
421
|
+
Napi::TypeError::New(env, "Window ID is required").ThrowAsJavaScriptException();
|
|
422
|
+
return env.Null();
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
uint32_t windowID = info[0].As<Napi::Number>().Uint32Value();
|
|
426
|
+
|
|
427
|
+
// Optional parameters
|
|
428
|
+
int maxWidth = 300; // Default thumbnail width
|
|
429
|
+
int maxHeight = 200; // Default thumbnail height
|
|
430
|
+
|
|
431
|
+
if (info.Length() >= 2 && !info[1].IsNull()) {
|
|
432
|
+
maxWidth = info[1].As<Napi::Number>().Int32Value();
|
|
433
|
+
}
|
|
434
|
+
if (info.Length() >= 3 && !info[2].IsNull()) {
|
|
435
|
+
maxHeight = info[2].As<Napi::Number>().Int32Value();
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
@try {
|
|
439
|
+
// Create window image
|
|
440
|
+
CGImageRef windowImage = CGWindowListCreateImage(
|
|
441
|
+
CGRectNull,
|
|
442
|
+
kCGWindowListOptionIncludingWindow,
|
|
443
|
+
windowID,
|
|
444
|
+
kCGWindowImageBoundsIgnoreFraming | kCGWindowImageShouldBeOpaque
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
if (!windowImage) {
|
|
448
|
+
return env.Null();
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Get original dimensions
|
|
452
|
+
size_t originalWidth = CGImageGetWidth(windowImage);
|
|
453
|
+
size_t originalHeight = CGImageGetHeight(windowImage);
|
|
454
|
+
|
|
455
|
+
// Calculate scaled dimensions maintaining aspect ratio
|
|
456
|
+
double scaleX = (double)maxWidth / originalWidth;
|
|
457
|
+
double scaleY = (double)maxHeight / originalHeight;
|
|
458
|
+
double scale = std::min(scaleX, scaleY);
|
|
459
|
+
|
|
460
|
+
size_t thumbnailWidth = (size_t)(originalWidth * scale);
|
|
461
|
+
size_t thumbnailHeight = (size_t)(originalHeight * scale);
|
|
462
|
+
|
|
463
|
+
// Create scaled image
|
|
464
|
+
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
|
|
465
|
+
CGContextRef context = CGBitmapContextCreate(
|
|
466
|
+
NULL,
|
|
467
|
+
thumbnailWidth,
|
|
468
|
+
thumbnailHeight,
|
|
469
|
+
8,
|
|
470
|
+
thumbnailWidth * 4,
|
|
471
|
+
colorSpace,
|
|
472
|
+
kCGImageAlphaPremultipliedLast
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
if (context) {
|
|
476
|
+
CGContextDrawImage(context, CGRectMake(0, 0, thumbnailWidth, thumbnailHeight), windowImage);
|
|
477
|
+
CGImageRef thumbnailImage = CGBitmapContextCreateImage(context);
|
|
478
|
+
|
|
479
|
+
if (thumbnailImage) {
|
|
480
|
+
// Convert to PNG data
|
|
481
|
+
NSBitmapImageRep *imageRep = [[NSBitmapImageRep alloc] initWithCGImage:thumbnailImage];
|
|
482
|
+
NSData *pngData = [imageRep representationUsingType:NSBitmapImageFileTypePNG properties:@{}];
|
|
483
|
+
|
|
484
|
+
if (pngData) {
|
|
485
|
+
// Convert to Base64
|
|
486
|
+
NSString *base64String = [pngData base64EncodedStringWithOptions:0];
|
|
487
|
+
std::string base64Std = [base64String UTF8String];
|
|
488
|
+
|
|
489
|
+
CGImageRelease(thumbnailImage);
|
|
490
|
+
CGContextRelease(context);
|
|
491
|
+
CGColorSpaceRelease(colorSpace);
|
|
492
|
+
CGImageRelease(windowImage);
|
|
493
|
+
|
|
494
|
+
return Napi::String::New(env, base64Std);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
CGImageRelease(thumbnailImage);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
CGContextRelease(context);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
CGColorSpaceRelease(colorSpace);
|
|
504
|
+
CGImageRelease(windowImage);
|
|
505
|
+
|
|
506
|
+
return env.Null();
|
|
507
|
+
|
|
508
|
+
} @catch (NSException *exception) {
|
|
509
|
+
return env.Null();
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// NAPI Function: Get Display Thumbnail
|
|
514
|
+
Napi::Value GetDisplayThumbnail(const Napi::CallbackInfo& info) {
|
|
515
|
+
Napi::Env env = info.Env();
|
|
516
|
+
|
|
517
|
+
if (info.Length() < 1) {
|
|
518
|
+
Napi::TypeError::New(env, "Display ID is required").ThrowAsJavaScriptException();
|
|
519
|
+
return env.Null();
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
uint32_t displayID = info[0].As<Napi::Number>().Uint32Value();
|
|
523
|
+
|
|
524
|
+
// Optional parameters
|
|
525
|
+
int maxWidth = 300; // Default thumbnail width
|
|
526
|
+
int maxHeight = 200; // Default thumbnail height
|
|
527
|
+
|
|
528
|
+
if (info.Length() >= 2 && !info[1].IsNull()) {
|
|
529
|
+
maxWidth = info[1].As<Napi::Number>().Int32Value();
|
|
530
|
+
}
|
|
531
|
+
if (info.Length() >= 3 && !info[2].IsNull()) {
|
|
532
|
+
maxHeight = info[2].As<Napi::Number>().Int32Value();
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
@try {
|
|
536
|
+
// Create display image
|
|
537
|
+
CGImageRef displayImage = CGDisplayCreateImage(displayID);
|
|
538
|
+
|
|
539
|
+
if (!displayImage) {
|
|
540
|
+
return env.Null();
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Get original dimensions
|
|
544
|
+
size_t originalWidth = CGImageGetWidth(displayImage);
|
|
545
|
+
size_t originalHeight = CGImageGetHeight(displayImage);
|
|
546
|
+
|
|
547
|
+
// Calculate scaled dimensions maintaining aspect ratio
|
|
548
|
+
double scaleX = (double)maxWidth / originalWidth;
|
|
549
|
+
double scaleY = (double)maxHeight / originalHeight;
|
|
550
|
+
double scale = std::min(scaleX, scaleY);
|
|
551
|
+
|
|
552
|
+
size_t thumbnailWidth = (size_t)(originalWidth * scale);
|
|
553
|
+
size_t thumbnailHeight = (size_t)(originalHeight * scale);
|
|
554
|
+
|
|
555
|
+
// Create scaled image
|
|
556
|
+
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
|
|
557
|
+
CGContextRef context = CGBitmapContextCreate(
|
|
558
|
+
NULL,
|
|
559
|
+
thumbnailWidth,
|
|
560
|
+
thumbnailHeight,
|
|
561
|
+
8,
|
|
562
|
+
thumbnailWidth * 4,
|
|
563
|
+
colorSpace,
|
|
564
|
+
kCGImageAlphaPremultipliedLast
|
|
565
|
+
);
|
|
566
|
+
|
|
567
|
+
if (context) {
|
|
568
|
+
CGContextDrawImage(context, CGRectMake(0, 0, thumbnailWidth, thumbnailHeight), displayImage);
|
|
569
|
+
CGImageRef thumbnailImage = CGBitmapContextCreateImage(context);
|
|
570
|
+
|
|
571
|
+
if (thumbnailImage) {
|
|
572
|
+
// Convert to PNG data
|
|
573
|
+
NSBitmapImageRep *imageRep = [[NSBitmapImageRep alloc] initWithCGImage:thumbnailImage];
|
|
574
|
+
NSData *pngData = [imageRep representationUsingType:NSBitmapImageFileTypePNG properties:@{}];
|
|
575
|
+
|
|
576
|
+
if (pngData) {
|
|
577
|
+
// Convert to Base64
|
|
578
|
+
NSString *base64String = [pngData base64EncodedStringWithOptions:0];
|
|
579
|
+
std::string base64Std = [base64String UTF8String];
|
|
580
|
+
|
|
581
|
+
CGImageRelease(thumbnailImage);
|
|
582
|
+
CGContextRelease(context);
|
|
583
|
+
CGColorSpaceRelease(colorSpace);
|
|
584
|
+
CGImageRelease(displayImage);
|
|
585
|
+
|
|
586
|
+
return Napi::String::New(env, base64Std);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
CGImageRelease(thumbnailImage);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
CGContextRelease(context);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
CGColorSpaceRelease(colorSpace);
|
|
596
|
+
CGImageRelease(displayImage);
|
|
597
|
+
|
|
598
|
+
return env.Null();
|
|
599
|
+
|
|
600
|
+
} @catch (NSException *exception) {
|
|
601
|
+
return env.Null();
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
416
605
|
// NAPI Function: Check Permissions
|
|
417
606
|
Napi::Value CheckPermissions(const Napi::CallbackInfo& info) {
|
|
418
607
|
Napi::Env env = info.Env();
|
|
@@ -466,6 +655,10 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
|
466
655
|
exports.Set(Napi::String::New(env, "getRecordingStatus"), Napi::Function::New(env, GetRecordingStatus));
|
|
467
656
|
exports.Set(Napi::String::New(env, "checkPermissions"), Napi::Function::New(env, CheckPermissions));
|
|
468
657
|
|
|
658
|
+
// Thumbnail functions
|
|
659
|
+
exports.Set(Napi::String::New(env, "getWindowThumbnail"), Napi::Function::New(env, GetWindowThumbnail));
|
|
660
|
+
exports.Set(Napi::String::New(env, "getDisplayThumbnail"), Napi::Function::New(env, GetDisplayThumbnail));
|
|
661
|
+
|
|
469
662
|
return exports;
|
|
470
663
|
}
|
|
471
664
|
|