node-mac-recorder 1.0.4 → 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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "1.0.4",
3
+ "version": "1.1.0",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -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();
@@ -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