108 lines
5.0 KiB
Objective-C
108 lines
5.0 KiB
Objective-C
// ScreenCaptureKit for mcap on macOS, behind a plain C interface (capture.h): ScreenCaptureKit is
|
|
// asynchronous, with completion blocks and its own queues, which is simpler to wait on here than from
|
|
// Jai. first.jai compiles this into capture.a beside it. Recording (macos-plan, phase 7) comes here too.
|
|
|
|
#import "capture.h"
|
|
#import <ScreenCaptureKit/ScreenCaptureKit.h>
|
|
#import <CoreGraphics/CoreGraphics.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
static void set_error(char *error, int error_size, NSString *text) {
|
|
if (!error || error_size <= 0) return;
|
|
const char *utf8 = text.UTF8String ? text.UTF8String : "unknown error";
|
|
strncpy(error, utf8, (size_t) error_size - 1);
|
|
error[error_size - 1] = 0;
|
|
}
|
|
|
|
// The image as packed RGBA, opaque, in sRGB (which the capture is already in): malloc'd.
|
|
static uint8_t *rgba_from_image(CGImageRef image, int32_t width, int32_t height) {
|
|
size_t row = (size_t) width * 4;
|
|
uint8_t *pixels = malloc(row * (size_t) height);
|
|
if (!pixels) return NULL;
|
|
CGColorSpaceRef srgb = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
|
|
CGContextRef bitmap = CGBitmapContextCreate(pixels, (size_t) width, (size_t) height, 8, row, srgb,
|
|
kCGBitmapByteOrder32Big | kCGImageAlphaNoneSkipLast);
|
|
CGColorSpaceRelease(srgb);
|
|
if (!bitmap) { free(pixels); return NULL; }
|
|
CGContextSetBlendMode(bitmap, kCGBlendModeCopy);
|
|
CGContextDrawImage(bitmap, CGRectMake(0, 0, width, height), image);
|
|
CGContextRelease(bitmap);
|
|
for (size_t i = 0; i < (size_t) width * (size_t) height; i++) pixels[i * 4 + 3] = 255;
|
|
return pixels;
|
|
}
|
|
|
|
int mcap_sck_screenshots(Mcap_Sck_Shot *shots, int count, char *error, int error_size) {
|
|
@autoreleasepool {
|
|
for (int i = 0; i < count; i++) shots[i].pixels = NULL;
|
|
|
|
__block SCShareableContent *content = nil;
|
|
__block NSError *content_error = nil;
|
|
dispatch_semaphore_t got = dispatch_semaphore_create(0);
|
|
[SCShareableContent getShareableContentExcludingDesktopWindows:NO onScreenWindowsOnly:YES
|
|
completionHandler:^(SCShareableContent *c, NSError *e) {
|
|
content = c;
|
|
content_error = e;
|
|
dispatch_semaphore_signal(got);
|
|
}];
|
|
if (dispatch_semaphore_wait(got, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC)) != 0) {
|
|
set_error(error, error_size, @"ScreenCaptureKit did not list the screens in time");
|
|
return 0;
|
|
}
|
|
if (!content) {
|
|
set_error(error, error_size, content_error ? content_error.localizedDescription : @"ScreenCaptureKit listed no screens");
|
|
return 0;
|
|
}
|
|
|
|
// Every screen at once; each completion fills its own entry.
|
|
dispatch_group_t group = dispatch_group_create();
|
|
__block NSString *failure = nil;
|
|
NSLock *lock = [NSLock new];
|
|
for (int i = 0; i < count; i++) {
|
|
SCDisplay *display = nil;
|
|
for (SCDisplay *d in content.displays) if (d.displayID == shots[i].display_id) display = d;
|
|
if (!display) {
|
|
failure = [NSString stringWithFormat:@"ScreenCaptureKit has no display %u", shots[i].display_id];
|
|
continue;
|
|
}
|
|
SCContentFilter *filter = [[SCContentFilter alloc] initWithDisplay:display excludingWindows:@[]];
|
|
SCStreamConfiguration *configuration = [SCStreamConfiguration new];
|
|
configuration.width = (size_t) shots[i].width;
|
|
configuration.height = (size_t) shots[i].height;
|
|
configuration.showsCursor = NO;
|
|
configuration.colorSpaceName = kCGColorSpaceSRGB;
|
|
configuration.captureResolution = SCCaptureResolutionBest;
|
|
Mcap_Sck_Shot *shot = &shots[i];
|
|
dispatch_group_enter(group);
|
|
[SCScreenshotManager captureImageWithFilter:filter configuration:configuration
|
|
completionHandler:^(CGImageRef image, NSError *e) {
|
|
if (image) {
|
|
int32_t w = (int32_t) CGImageGetWidth(image), h = (int32_t) CGImageGetHeight(image);
|
|
shot->width = w;
|
|
shot->height = h;
|
|
shot->pixels = rgba_from_image(image, w, h);
|
|
}
|
|
if (!shot->pixels) {
|
|
[lock lock];
|
|
failure = e ? e.localizedDescription : @"ScreenCaptureKit gave no image";
|
|
[lock unlock];
|
|
}
|
|
dispatch_group_leave(group);
|
|
}];
|
|
}
|
|
if (dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)) != 0) {
|
|
failure = @"ScreenCaptureKit did not copy the screens in time";
|
|
}
|
|
if (failure) {
|
|
// Entries still being filled after a timeout are left to leak rather than raced.
|
|
set_error(error, error_size, failure);
|
|
return 0;
|
|
}
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
void mcap_sck_free(void *pointer) {
|
|
free(pointer);
|
|
}
|