19#import <Foundation/Foundation.h>
20#import <CoreMedia/CoreMedia.h>
21#import <ScreenCaptureKit/ScreenCaptureKit.h>
25#include <winpr/assert.h>
27#include <freerdp/log.h>
29#include "mac_capture.h"
31#define TAG SERVER_TAG("shadow.mac")
35static const int64_t mac_capture_timeout_ns = 10LL * NSEC_PER_SEC;
37@interface MacCaptureOutput : NSObject <SCStreamOutput, SCStreamDelegate>
42 size_t audioBufferFrames;
51 MacCaptureOutput *output;
53 dispatch_queue_t videoQueue;
54 dispatch_queue_t audioQueue;
58static void mac_capture_log_error(
const char *what, NSError *error)
61 WLog_ERR(TAG,
"%s: %s (%s %ld)", what, error.localizedDescription.UTF8String,
62 error.domain.UTF8String, (
long)error.code);
64 WLog_ERR(TAG,
"%s: timed out", what);
67@implementation MacCaptureOutput
74- (void)onScreen:(CMSampleBufferRef)sampleBuffer
76 CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer,
false);
77 if (!attachments || (CFArrayGetCount(attachments) < 1))
80 NSDictionary *info = (__bridge NSDictionary *)CFArrayGetValueAtIndex(attachments, 0);
81 NSNumber *status = info[SCStreamFrameInfoStatus];
84 if (!status || (status.integerValue != SCFrameStatusComplete))
87 CVPixelBufferRef frame = CMSampleBufferGetImageBuffer(sampleBuffer);
91 NSArray *dirty = info[SCStreamFrameInfoDirtyRects];
92 const size_t count = dirty.count;
93 CGRect stackRects[32];
94 CGRect *rects = stackRects;
96 if (count > ARRAYSIZE(stackRects))
98 rects = calloc(count,
sizeof(CGRect));
104 for (NSDictionary *value in dirty)
106 CGRect rect = CGRectNull;
107 if (CGRectMakeWithDictionaryRepresentation((__bridge CFDictionaryRef)value, &rect))
108 rects[used++] = rect;
114 rects[0] = CGRectMake(0, 0, (CGFloat)CVPixelBufferGetWidth(frame),
115 (CGFloat)CVPixelBufferGetHeight(frame));
119 config.onFrame(config.context, frame, rects, used);
121 if (rects != stackRects)
125- (void)onAudio:(CMSampleBufferRef)sampleBuffer
130 CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);
131 const AudioStreamBasicDescription *asbd =
132 format ? CMAudioFormatDescriptionGetStreamBasicDescription(format) : NULL;
133 if (!asbd || (asbd->mFormatID != kAudioFormatLinearPCM) ||
134 !(asbd->mFormatFlags & kAudioFormatFlagIsFloat) || (asbd->mBitsPerChannel != 32))
137 const UINT32 channels = asbd->mChannelsPerFrame;
138 const size_t frames = (size_t)CMSampleBufferGetNumSamples(sampleBuffer);
139 if ((channels == 0) || (frames == 0))
144 if (CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(sampleBuffer, &listSize, NULL, 0,
145 NULL, NULL, 0, NULL) != noErr)
148 AudioBufferList *list = calloc(1, listSize);
152 CMBlockBufferRef block = NULL;
153 if (CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(
154 sampleBuffer, NULL, list, listSize, NULL, NULL,
155 kCMSampleBufferFlag_AudioBufferList_Assure16ByteAlignment, &block) != noErr)
161 if (audioBufferFrames < frames)
163 float *tmp = realloc(audioBuffer, frames * channels *
sizeof(
float));
167 audioBufferFrames = frames;
170 const BOOL interleaved = !(asbd->mFormatFlags & kAudioFormatFlagIsNonInterleaved);
173 if ((list->mNumberBuffers < 1) ||
174 (list->mBuffers[0].mDataByteSize < frames * channels *
sizeof(float)))
176 memcpy(audioBuffer, list->mBuffers[0].mData, frames * channels *
sizeof(
float));
180 if (list->mNumberBuffers < channels)
182 for (UINT32 c = 0; c < channels; c++)
184 const float *src = list->mBuffers[c].mData;
185 if (list->mBuffers[c].mDataByteSize < frames *
sizeof(
float))
187 for (
size_t f = 0; f < frames; f++)
188 audioBuffer[f * channels + c] = src[f];
192 config.onAudio(config.context, audioBuffer, frames, channels, (UINT32)asbd->mSampleRate);
200- (void)stream:(SCStream *)stream
201 didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
202 ofType:(SCStreamOutputType)type
204 WINPR_UNUSED(stream);
206 if (!CMSampleBufferIsValid(sampleBuffer))
211 case SCStreamOutputTypeScreen:
212 [
self onScreen:sampleBuffer];
214 case SCStreamOutputTypeAudio:
215 [
self onAudio:sampleBuffer];
222- (void)stream:(SCStream *)stream didStopWithError:(NSError *)error
224 WINPR_UNUSED(stream);
225 atomic_store(&stopped,
true);
226 WLog_WARN(TAG,
"screen capture stopped: %s (%s %ld)",
227 error ? error.localizedDescription.UTF8String :
"no reason given",
228 error ? error.domain.UTF8String :
"-", error ? (long)error.code : 0L);
229 if (config.onStopped)
230 config.onStopped(config.context);
235BOOL mac_capture_check_access(
void)
237 if (CGPreflightScreenCaptureAccess())
240 WLog_ERR(TAG,
"no permission to record the screen: allow this program in System Settings > "
241 "Privacy & Security > Screen & System Audio Recording, then start it again");
242 (void)CGRequestScreenCaptureAccess();
246static SCDisplay *mac_capture_find_display(CGDirectDisplayID displayID)
248 __block SCShareableContent *content = nil;
249 __block NSError *error = nil;
250 dispatch_semaphore_t done = dispatch_semaphore_create(0);
253 getShareableContentExcludingDesktopWindows:NO
254 onScreenWindowsOnly:YES
255 completionHandler:^(SCShareableContent *c, NSError *e) {
258 dispatch_semaphore_signal(done);
261 if (dispatch_semaphore_wait(done, dispatch_time(DISPATCH_TIME_NOW, mac_capture_timeout_ns)) !=
267 mac_capture_log_error(
"the list of displays", error);
271 for (SCDisplay *display in content.displays)
273 if (display.displayID == displayID)
277 WLog_ERR(TAG,
"display %" PRIu32
" is not among the ones that can be captured", displayID);
283 WINPR_ASSERT(config);
284 WINPR_ASSERT(config->onFrame);
286 macCapture *capture = calloc(1,
sizeof(macCapture));
290 capture->config = *config;
294 SCDisplay *display = mac_capture_find_display(config->display);
298 SCContentFilter *filter = [[SCContentFilter alloc] initWithDisplay:display
299 excludingWindows:@[]];
301 SCStreamConfiguration *streamConfig = [[SCStreamConfiguration alloc] init];
302 streamConfig.width = config->width;
303 streamConfig.height = config->height;
304 streamConfig.pixelFormat = kCVPixelFormatType_32BGRA;
305 streamConfig.colorSpaceName = kCGColorSpaceSRGB;
306 streamConfig.minimumFrameInterval = CMTimeMake(1, (int32_t)MAX(config->fps, 1));
307 streamConfig.showsCursor = config->showCursor;
310 streamConfig.queueDepth = 5;
314 if (@available(macOS 13.0, *))
316 streamConfig.capturesAudio = YES;
317 streamConfig.excludesCurrentProcessAudio = YES;
318 streamConfig.sampleRate = (NSInteger)config->audioRate;
319 streamConfig.channelCount = (NSInteger)config->audioChannels;
323 WLog_WARN(TAG,
"sound needs macOS 13 or later: no sound");
324 capture->config.audio = FALSE;
328 capture->output = [[MacCaptureOutput alloc] init];
329 capture->output->config = capture->config;
330 capture->stream = [[SCStream alloc] initWithFilter:filter
331 configuration:streamConfig
332 delegate:capture->output];
333 if (!capture->stream)
336 capture->videoQueue = dispatch_queue_create(
"freerdp.shadow.mac.video", NULL);
337 NSError *error = nil;
338 if (![capture->stream addStreamOutput:capture->output
339 type:SCStreamOutputTypeScreen
340 sampleHandlerQueue:capture->videoQueue
343 mac_capture_log_error(
"screen output", error);
347 if (capture->config.audio)
349 if (@available(macOS 13.0, *))
351 capture->audioQueue = dispatch_queue_create(
"freerdp.shadow.mac.audio", NULL);
352 if (![capture->stream addStreamOutput:capture->output
353 type:SCStreamOutputTypeAudio
354 sampleHandlerQueue:capture->audioQueue
357 mac_capture_log_error(
"sound output: no sound", error);
358 capture->output->config.onAudio = NULL;
367 mac_capture_free(capture);
371static BOOL mac_capture_wait(
const char *what,
void (^call)(
void (^)(NSError *)))
373 __block NSError *error = nil;
374 __block BOOL answered = NO;
375 dispatch_semaphore_t done = dispatch_semaphore_create(0);
380 dispatch_semaphore_signal(done);
383 if (dispatch_semaphore_wait(done, dispatch_time(DISPATCH_TIME_NOW, mac_capture_timeout_ns)) !=
387 mac_capture_log_error(what, answered ? error : nil);
393BOOL mac_capture_start(macCapture *capture)
395 if (!capture || !capture->stream)
397 if (capture->running)
400 SCStream *stream = capture->stream;
401 capture->running = mac_capture_wait(
"starting the screen capture", ^(
void (^done)(NSError *)) {
402 [stream startCaptureWithCompletionHandler:done];
404 return capture->running;
407void mac_capture_stop(macCapture *capture)
409 if (!capture || !capture->stream || !capture->running)
411 if (atomic_load(&capture->output->stopped))
413 capture->running = FALSE;
417 SCStream *stream = capture->stream;
418 (void)mac_capture_wait(
"stopping the screen capture", ^(
void (^done)(NSError *)) {
419 [stream stopCaptureWithCompletionHandler:done];
421 capture->running = FALSE;
424void mac_capture_free(macCapture *capture)
429 mac_capture_stop(capture);
433 capture->stream = nil;
434 capture->output = nil;
435 capture->videoQueue = nil;
436 capture->audioQueue = nil;