Roman Pushkin
Roman Pushkin

Reputation: 6089

ScreenCaptureKit example in Go/C

I'm working on improving open source library to capture screenshots: https://github.com/kbinani/screenshot - it is somewhat popular and it uses legacy approach, and MacOS 15 (beta as of time of writing) already complains that "CGDisplayCreateImageForRect' is unavailable: obsoleted in macOS 15.0". So you can't use it on macOS 15.0 - will not compile.

I spent a couple of days trying to use ScreenCaptureKit from Apple. Which is a new way of doing things. And it works, but not in Go with C bindings.

How do I implement SCScreenshotManager_captureImage in C99?

I have working code in swift (works absolutely fine):

import Foundation
import ScreenCaptureKit
import CoreGraphics
import AppKit

// This program captures a screenshot using Apple's ScreenCaptureKit framework.
// It detects multiple monitors and allows capturing from a specific display or the main display.

// To run this program:
// 1. Make sure you have Xcode and Swift installed on your Mac.
// 2. Save this code in a file named 'demo.swift'.
// 3. Open Terminal, navigate to the directory containing the file.
// 4. Run: swift demo.swift

// Function to capture screenshot
func captureScreenshot(display: SCDisplay?, completion: @escaping (NSImage?) -> Void) {
    let filter: SCContentFilter
    if let display = display {
        filter = SCContentFilter(display: display, excludingApplications: [], exceptingWindows: [])
    } else {
        filter = SCContentFilter()
    }
    
    let configuration = SCStreamConfiguration()
    configuration.width = 1920 // You can adjust this
    configuration.height = 1080 // You can adjust this
    configuration.showsCursor = true
    configuration.scalesToFit = false
    
    SCScreenshotManager.captureImage(contentFilter: filter, configuration: configuration) { image, error in
        if let error = error {
            print("Error capturing screenshot: \(error.localizedDescription)")
            completion(nil)
        } else if let image = image {
            completion(NSImage(cgImage: image, size: NSSize(width: image.width, height: image.height)))
        } else {
            print("No image captured")
            completion(nil)
        }
    }
}

// Main execution
func main() {
    let semaphore = DispatchSemaphore(value: 0)
    
    Task {
        do {
            let content = try await SCShareableContent.current
            
            guard !content.displays.isEmpty else {
                print("No displays found")
                semaphore.signal()
                return
            }
            
            print("Available displays:")
            for (index, display) in content.displays.enumerated() {
                print("\(index + 1). \(display.displayID) - \(display.width)x\(display.height)")
            }
            
            print("Enter the number of the display to capture (or press Enter for main display):")
            let input = readLine()?.trimmingCharacters(in: .whitespacesAndNewlines)
            
            let selectedDisplay: SCDisplay?
            if let index = Int(input ?? ""), (1...content.displays.count).contains(index) {
                selectedDisplay = content.displays[index - 1]
            } else {
                selectedDisplay = content.displays.first
            }
            
            captureScreenshot(display: selectedDisplay) { image in
                if let image = image {
                    let currentDirectoryURL = URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
                    let fileURL = currentDirectoryURL.appendingPathComponent("screenshot.png")
                    
                    if let tiffData = image.tiffRepresentation,
                       let bitmapImage = NSBitmapImageRep(data: tiffData),
                       let pngData = bitmapImage.representation(using: .png, properties: [:]) {
                        do {
                            try pngData.write(to: fileURL)
                            print("Screenshot saved to: \(fileURL.path)")
                        } catch {
                            print("Error saving screenshot: \(error.localizedDescription)")
                        }
                    } else {
                        print("Error converting image to PNG data")
                    }
                } else {
                    print("Failed to capture screenshot")
                }
                semaphore.signal()
            }
        } catch {
            print("Error getting shareable content: \(error.localizedDescription)")
            semaphore.signal()
        }
    }
    
    semaphore.wait()
}

main()

But when it comes to making a Go package that just works - nothing works. I've spent hours trying to make it work. For Stackoverflow I created GitHub repo to demonstrate the problem with instructions: https://github.com/ro31337/screenshot_macos

The main_screencapturekit.go has the method that is not implemented (well, I tried to implement it , but ended up with solution that just sits there and does nothing, I don't provide these extra few lines so you're not derailed):

CGImageRef SCScreenshotManager_captureImage(SCContentFilter* filter, SCStreamConfiguration* config) {
    __block CGImageRef capturedImage = NULL;
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

    NSLog(@"Initializing SCStream...");
    SCStream* stream = [[SCStream alloc] initWithFilter:filter configuration:config delegate:nil];

    NSLog(@"Starting capture...");
    [stream startCaptureWithCompletionHandler:^(NSError * _Nullable error) {
        if (error) {
            NSLog(@"Error starting capture: %@", error.localizedDescription);
            NSLog(@"Error domain: %@", error.domain);
            NSLog(@"Error code: %ld", (long)error.code);
            NSLog(@"Error user info: %@", error.userInfo);
            dispatch_semaphore_signal(semaphore);
        } else {
            NSLog(@"Capture started successfully, attempting to capture screenshot...");
            [stream stopCaptureWithCompletionHandler:^(NSError * _Nullable error) {
                if (error) {
                    NSLog(@"Error stopping capture: %@", error.localizedDescription);
                    NSLog(@"Error domain: %@", error.domain);
                    NSLog(@"Error code: %ld", (long)error.code);
                    NSLog(@"Error user info: %@", error.userInfo);
                } else {
                    NSLog(@"Capture stopped successfully");
                }
                dispatch_semaphore_signal(semaphore);
            }];
        }
    }];

    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    [stream release];
    return capturedImage;
}

It's currently producing good output and new ScreenCaptureKit bindings do work (with whole bunch of warnings I wasn't able to get rid of):

2024-08-07 19:16:31.094 main_screencapturekit[10934:95763] Number of displays: 1
2024-08-07 19:16:31.152 main_screencapturekit[10934:95763] Display info - Index: 0, Width: 1440, Height: 900, X: 0, Y: 0, ID: 69734272
2024-08-07 19:16:31.206 main_screencapturekit[10934:95763] Display info - Index: 0, Width: 1440, Height: 900, X: 0, Y: 0, ID: 69734272
2024-08-07 19:16:31.259 main_screencapturekit[10934:95763] SCContentFilter created successfully for display ID: 69734272
2024-08-07 19:16:31.259 main_screencapturekit[10934:95759] SCStreamConfiguration initialized with width: 1920, height: 1080
2024-08-07 19:16:31.259 main_screencapturekit[10934:95759] SCStreamConfiguration width set to: 1440
2024-08-07 19:16:31.259 main_screencapturekit[10934:95759] SCStreamConfiguration height set to: 900
2024-08-07 19:16:31.259 main_screencapturekit[10934:95759] SCStreamConfiguration showsCursor set to: 0
2024-08-07 19:16:31.259 main_screencapturekit[10934:95759] Initializing SCStream...
2024-08-07 19:16:31.371 main_screencapturekit[10934:95759] Starting capture...
2024-08-07 19:16:31.421 main_screencapturekit[10934:95764] Capture started successfully, attempting to capture screenshot...
2024-08-07 19:16:31.424 main_screencapturekit[10934:95825] Capture stopped successfully
#0 : (0,0)-(1440,900) "0_1440x900.png"
(0,0)-(1440,900)
2024-08-07 19:16:31.510 main_screencapturekit[10934:95825] Display info - Index: 0, Width: 1440, Height: 900, X: 0, Y: 0, ID: 69734272
2024-08-07 19:16:31.566 main_screencapturekit[10934:95825] SCContentFilter created successfully for display ID: 69734272
2024-08-07 19:16:31.566 main_screencapturekit[10934:95759] SCStreamConfiguration initialized with width: 1920, height: 1080
2024-08-07 19:16:31.566 main_screencapturekit[10934:95759] SCStreamConfiguration width set to: 1440
2024-08-07 19:16:31.566 main_screencapturekit[10934:95759] SCStreamConfiguration height set to: 900
2024-08-07 19:16:31.566 main_screencapturekit[10934:95759] SCStreamConfiguration showsCursor set to: 0
2024-08-07 19:16:31.566 main_screencapturekit[10934:95759] Initializing SCStream...
2024-08-07 19:16:31.567 main_screencapturekit[10934:95759] Starting capture...
2024-08-07 19:16:31.610 main_screencapturekit[10934:95764] Capture started successfully, attempting to capture screenshot...
2024-08-07 19:16:31.612 main_screencapturekit[10934:95824] Capture stopped successfully

So we definitely have progress. Bindings do work. But now we have to capture the stream I guess? But how? I don't even know where to look.

I hope somebody out there can help me out, so we can improve the cross-platform screenshot making library for newest MacOS.

Upvotes: 0

Views: 525

Answers (0)

Related Questions