// 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 #import #include #include 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); }