mirror of
https://github.com/libretro/Libretro-Supermodel.git
synced 2026-11-08 08:09:41 -10:00
214 lines
7.8 KiB
Objective-C
214 lines
7.8 KiB
Objective-C
#import <AVFoundation/AVFoundation.h>
|
|
#import <AppKit/AppKit.h>
|
|
#import <Foundation/Foundation.h>
|
|
#import <ScreenCaptureKit/ScreenCaptureKit.h>
|
|
#include <errno.h>
|
|
#include <signal.h>
|
|
#include <unistd.h>
|
|
|
|
@interface RecordingDelegate : NSObject <SCRecordingOutputDelegate>
|
|
@property(nonatomic, strong) NSError *failure;
|
|
@property(nonatomic) BOOL started;
|
|
@property(nonatomic) BOOL finished;
|
|
@end
|
|
|
|
@implementation RecordingDelegate
|
|
- (void)recordingOutputDidStartRecording:(SCRecordingOutput *)recordingOutput
|
|
{
|
|
self.started = YES;
|
|
}
|
|
- (void)recordingOutput:(SCRecordingOutput *)recordingOutput
|
|
didFailWithError:(NSError *)error
|
|
{
|
|
self.failure = error;
|
|
self.finished = YES;
|
|
}
|
|
- (void)recordingOutputDidFinishRecording:(SCRecordingOutput *)recordingOutput
|
|
{
|
|
self.finished = YES;
|
|
}
|
|
@end
|
|
|
|
static BOOL ProcessExists(pid_t pid)
|
|
{
|
|
return kill(pid, 0) == 0 || errno == EPERM;
|
|
}
|
|
|
|
static SCShareableContent *GetShareableContent(NSError **outError)
|
|
{
|
|
__block SCShareableContent *result = nil;
|
|
__block NSError *failure = nil;
|
|
dispatch_semaphore_t done = dispatch_semaphore_create(0);
|
|
[SCShareableContent getShareableContentExcludingDesktopWindows:YES
|
|
onScreenWindowsOnly:YES
|
|
completionHandler:^(SCShareableContent *content, NSError *error) {
|
|
result = content;
|
|
failure = error;
|
|
dispatch_semaphore_signal(done);
|
|
}];
|
|
dispatch_semaphore_wait(done, DISPATCH_TIME_FOREVER);
|
|
if (outError)
|
|
*outError = failure;
|
|
return result;
|
|
}
|
|
|
|
static SCWindow *FindWindow(pid_t pid, SCDisplay **outDisplay, NSError **outError)
|
|
{
|
|
NSDate *deadline = [NSDate dateWithTimeIntervalSinceNow:15.0];
|
|
do {
|
|
NSError *error = nil;
|
|
SCShareableContent *content = GetShareableContent(&error);
|
|
if (!content) {
|
|
if (outError)
|
|
*outError = error;
|
|
return nil;
|
|
}
|
|
|
|
SCWindow *best = nil;
|
|
CGFloat bestArea = 0.0;
|
|
for (SCWindow *window in content.windows) {
|
|
if (window.owningApplication.processID != pid ||
|
|
window.frame.size.width <= 100.0 ||
|
|
window.frame.size.height <= 100.0)
|
|
continue;
|
|
CGFloat area = window.frame.size.width * window.frame.size.height;
|
|
if (area > bestArea) {
|
|
best = window;
|
|
bestArea = area;
|
|
}
|
|
}
|
|
if (best) {
|
|
SCDisplay *selected = nil;
|
|
for (SCDisplay *display in content.displays) {
|
|
if (CGRectIntersectsRect(display.frame, best.frame)) {
|
|
selected = display;
|
|
break;
|
|
}
|
|
}
|
|
if (!selected)
|
|
selected = content.displays.firstObject;
|
|
if (outDisplay)
|
|
*outDisplay = selected;
|
|
return best;
|
|
}
|
|
usleep(100000);
|
|
} while ([deadline timeIntervalSinceNow] > 0.0 && ProcessExists(pid));
|
|
return nil;
|
|
}
|
|
|
|
static void PrintError(NSString *message)
|
|
{
|
|
fprintf(stderr, "error: %s\n", message.UTF8String);
|
|
}
|
|
|
|
int main(int argc, const char *argv[])
|
|
{
|
|
@autoreleasepool {
|
|
[NSApplication sharedApplication];
|
|
[NSApp setActivationPolicy:NSApplicationActivationPolicyProhibited];
|
|
|
|
pid_t pid = 0;
|
|
NSString *outputPath = nil;
|
|
for (int index = 1; index < argc; ++index) {
|
|
if (strcmp(argv[index], "--pid") == 0 && index + 1 < argc)
|
|
pid = (pid_t)strtol(argv[++index], NULL, 10);
|
|
else if (strcmp(argv[index], "--output") == 0 && index + 1 < argc)
|
|
outputPath = [NSString stringWithUTF8String:argv[++index]];
|
|
else {
|
|
PrintError(@"usage: ScreenCaptureWindow --pid PID --output FILE.mov");
|
|
return 2;
|
|
}
|
|
}
|
|
if (pid <= 0 || outputPath.length == 0) {
|
|
PrintError(@"usage: ScreenCaptureWindow --pid PID --output FILE.mov");
|
|
return 2;
|
|
}
|
|
|
|
__block NSError *error = nil;
|
|
SCDisplay *display = nil;
|
|
SCWindow *window = FindWindow(pid, &display, &error);
|
|
if (!window) {
|
|
PrintError(error.localizedDescription ?: [NSString stringWithFormat:
|
|
@"no capturable on-screen window appeared for PID %d", pid]);
|
|
return 4;
|
|
}
|
|
|
|
CGFloat scale = 1.0;
|
|
if (display && display.frame.size.width > 0.0)
|
|
scale = MAX(1.0, (CGFloat)display.width / display.frame.size.width);
|
|
size_t width = MAX(2, (size_t)llround(window.frame.size.width * scale));
|
|
size_t height = MAX(2, (size_t)llround(window.frame.size.height * scale));
|
|
width &= ~(size_t)1;
|
|
height &= ~(size_t)1;
|
|
|
|
SCStreamConfiguration *configuration = [[SCStreamConfiguration alloc] init];
|
|
configuration.width = width;
|
|
configuration.height = height;
|
|
configuration.minimumFrameInterval = CMTimeMake(1, 30);
|
|
configuration.queueDepth = 5;
|
|
configuration.showsCursor = NO;
|
|
configuration.capturesAudio = NO;
|
|
|
|
NSURL *outputURL = [NSURL fileURLWithPath:outputPath];
|
|
[[NSFileManager defaultManager] removeItemAtURL:outputURL error:nil];
|
|
SCRecordingOutputConfiguration *recordingConfiguration =
|
|
[[SCRecordingOutputConfiguration alloc] init];
|
|
recordingConfiguration.outputURL = outputURL;
|
|
recordingConfiguration.videoCodecType = AVVideoCodecTypeH264;
|
|
recordingConfiguration.outputFileType = AVFileTypeQuickTimeMovie;
|
|
|
|
RecordingDelegate *delegate = [[RecordingDelegate alloc] init];
|
|
SCRecordingOutput *recordingOutput =
|
|
[[SCRecordingOutput alloc] initWithConfiguration:recordingConfiguration
|
|
delegate:delegate];
|
|
SCContentFilter *filter =
|
|
[[SCContentFilter alloc] initWithDesktopIndependentWindow:window];
|
|
SCStream *stream = [[SCStream alloc] initWithFilter:filter
|
|
configuration:configuration
|
|
delegate:nil];
|
|
if (![stream addRecordingOutput:recordingOutput error:&error]) {
|
|
PrintError(error.localizedDescription);
|
|
return 5;
|
|
}
|
|
|
|
dispatch_semaphore_t started = dispatch_semaphore_create(0);
|
|
[stream startCaptureWithCompletionHandler:^(NSError *startError) {
|
|
error = startError;
|
|
dispatch_semaphore_signal(started);
|
|
}];
|
|
dispatch_semaphore_wait(started, DISPATCH_TIME_FOREVER);
|
|
if (error) {
|
|
PrintError(error.localizedDescription);
|
|
return 6;
|
|
}
|
|
while (ProcessExists(pid) && !delegate.failure)
|
|
usleep(100000);
|
|
|
|
__block NSError *stopFailure = nil;
|
|
dispatch_semaphore_t stopped = dispatch_semaphore_create(0);
|
|
[stream stopCaptureWithCompletionHandler:^(NSError *stopError) {
|
|
stopFailure = stopError;
|
|
dispatch_semaphore_signal(stopped);
|
|
}];
|
|
dispatch_semaphore_wait(stopped, DISPATCH_TIME_FOREVER);
|
|
|
|
NSDate *finishDeadline = [NSDate dateWithTimeIntervalSinceNow:10.0];
|
|
while (!delegate.finished && [finishDeadline timeIntervalSinceNow] > 0.0)
|
|
usleep(50000);
|
|
if (delegate.failure)
|
|
error = delegate.failure;
|
|
BOOL hasOutput = [[NSFileManager defaultManager] fileExistsAtPath:outputPath];
|
|
if (!hasOutput) {
|
|
PrintError((error ?: stopFailure).localizedDescription ?:
|
|
@"ScreenCaptureKit produced no video file");
|
|
return 8;
|
|
}
|
|
if (error) {
|
|
PrintError(error.localizedDescription);
|
|
return 7;
|
|
}
|
|
printf("captured %zux%zu window video\n", width, height);
|
|
return 0;
|
|
}
|
|
}
|