FreeRDP
Loading...
Searching...
No Matches
mac_shadow.c
1
20#include <math.h>
21#include <pwd.h>
22#include <unistd.h>
23
24#include <winpr/assert.h>
25#include <winpr/cast.h>
26#include <winpr/crt.h>
27#include <winpr/synch.h>
28#include <winpr/input.h>
29#include <winpr/sysinfo.h>
30
31#include <freerdp/server/server-common.h>
32#include <freerdp/codec/audio.h>
33#include <freerdp/codec/color.h>
34#include <freerdp/codec/dsp.h>
35#include <freerdp/codec/region.h>
36#include <freerdp/log.h>
37
38#include <ApplicationServices/ApplicationServices.h>
39
40#ifdef WITH_PAM
41#include <security/pam_appl.h>
42#endif
43
44#include "mac_shadow.h"
45
46#define TAG SERVER_TAG("shadow.mac")
47
48/* Frames per second asked from ScreenCaptureKit at most */
49#define MAC_SHADOW_FPS 30
50/* how often to look for the display again after the stream ended */
51#define MAC_SHADOW_RESTART_MS 1000
52/* how often to look at the displays: a reconfiguration callback only reaches a
53 * process with an application's connection to the window server, and this
54 * one is a command line tool */
55#define MAC_SHADOW_DISPLAYS_MS 1000
56/* how often to look at the cursor, and how long after a client moved the
57 * pointer not to tell it where the pointer is: its own cursor is ahead of
58 * what the server sees by a round trip, and would jump back */
59#define MAC_SHADOW_CURSOR_MS 30
60#define MAC_SHADOW_CURSOR_QUIET_MS 500
61/* the largest cursor sent, in points */
62#define MAC_SHADOW_CURSOR_MAX 96
63/* how often the sound statistics are logged (debug level) */
64#define MAC_SHADOW_AUDIO_LOG_MS 5000
65
66/* Two clicks closer than this, in time and in points, make a double click */
67#define MAC_SHADOW_CLICK_MS 500
68#define MAC_SHADOW_CLICK_DISTANCE 4.0
69
70/* What the clients hear: 48 kHz, 2 channels, 16 bits, the format
71 * ScreenCaptureKit is asked for. The build may have no resampler, so every
72 * format offered has that rate and channel count, and 16 bits a sample:
73 * rdpsnd sizes its buffers from the bits and channels of the first one. Opus
74 * first, so that a client that has it takes it (96 kbit/s instead of
75 * 1.5 Mbit/s for PCM). Its block alignment is 1: rdpsnd pads every wave to
76 * a multiple of it with zeros, and an Opus packet with zeros after it no
77 * longer decodes. */
78#define MAC_AUDIO_RATE 48000
79#define MAC_AUDIO_CHANNELS 2
80#define MAC_AUDIO_FRAME_BYTES (MAC_AUDIO_CHANNELS * 2)
81/* samples are sent in messages of 20 ms */
82#define MAC_AUDIO_MESSAGE_FRAMES (MAC_AUDIO_RATE / 50)
83
84static const AUDIO_FORMAT mac_audio_offered_formats[] = {
85 { WAVE_FORMAT_OPUS, MAC_AUDIO_CHANNELS, MAC_AUDIO_RATE, 12000, 1, 16, 0, nullptr },
86 { WAVE_FORMAT_PCM, MAC_AUDIO_CHANNELS, MAC_AUDIO_RATE, (MAC_AUDIO_RATE * MAC_AUDIO_FRAME_BYTES),
87 MAC_AUDIO_FRAME_BYTES, 16, 0, nullptr },
88};
89
90/* The format of the samples in each message: it outlives every client, since
91 * rdpsnd keeps the pointer as its source format */
92static AUDIO_FORMAT mac_audio_source_format = { WAVE_FORMAT_PCM,
93 MAC_AUDIO_CHANNELS,
94 MAC_AUDIO_RATE,
95 (MAC_AUDIO_RATE * MAC_AUDIO_FRAME_BYTES),
96 MAC_AUDIO_FRAME_BYTES,
97 16,
98 0,
99 nullptr };
100
101/* Apple virtual key codes of the modifier keys (HIToolbox/Events.h) */
102enum
103{
104 MAC_VK_COMMAND = 0x37,
105 MAC_VK_SHIFT = 0x38,
106 MAC_VK_CAPSLOCK = 0x39,
107 MAC_VK_OPTION = 0x3A,
108 MAC_VK_CONTROL = 0x3B,
109 MAC_VK_RIGHT_COMMAND = 0x36,
110 MAC_VK_RIGHT_SHIFT = 0x3C,
111 MAC_VK_RIGHT_OPTION = 0x3D,
112 MAC_VK_RIGHT_CONTROL = 0x3E,
113 MAC_VK_FUNCTION = 0x3F
114};
115
116static CGEventFlags mac_shadow_modifier_flag(CGKeyCode keycode)
117{
118 switch (keycode)
119 {
120 case MAC_VK_SHIFT:
121 case MAC_VK_RIGHT_SHIFT:
122 return kCGEventFlagMaskShift;
123 case MAC_VK_CONTROL:
124 case MAC_VK_RIGHT_CONTROL:
125 return kCGEventFlagMaskControl;
126 case MAC_VK_OPTION:
127 case MAC_VK_RIGHT_OPTION:
128 return kCGEventFlagMaskAlternate;
129 case MAC_VK_COMMAND:
130 case MAC_VK_RIGHT_COMMAND:
131 return kCGEventFlagMaskCommand;
132 case MAC_VK_FUNCTION:
133 return kCGEventFlagMaskSecondaryFn;
134 default:
135 return 0;
136 }
137}
138
139/* The active displays, with their monitor rectangles moved so that the
140 * leftmost and topmost one start at 0 (the shadow surface cannot start
141 * below 0); bounds are left in global display coordinates. */
142static UINT32 mac_shadow_list_displays(MONITOR_DEF* monitors, CGDirectDisplayID* ids,
143 CGRect* bounds, UINT32 maxMonitors)
144{
145 CGDirectDisplayID active[MAC_SHADOW_MAX_DISPLAYS] = WINPR_C_ARRAY_INIT;
146 uint32_t count = 0;
147
148 if (CGGetActiveDisplayList(ARRAYSIZE(active), active, &count) != kCGErrorSuccess)
149 return 0;
150
151 count = MIN(count, maxMonitors);
152 CGFloat minX = 0;
153 CGFloat minY = 0;
154
155 for (uint32_t x = 0; x < count; x++)
156 {
157 const CGRect rect = CGDisplayBounds(active[x]);
158 minX = MIN(minX, rect.origin.x);
159 minY = MIN(minY, rect.origin.y);
160 }
161
162 const CGDirectDisplayID mainDisplay = CGMainDisplayID();
163 for (uint32_t x = 0; x < count; x++)
164 {
165 const CGRect rect = CGDisplayBounds(active[x]);
166 MONITOR_DEF* monitor = &monitors[x];
167 monitor->left = (INT32)(rect.origin.x - minX);
168 monitor->top = (INT32)(rect.origin.y - minY);
169 monitor->right = monitor->left + (INT32)rect.size.width - 1;
170 monitor->bottom = monitor->top + (INT32)rect.size.height - 1;
171 monitor->flags = (active[x] == mainDisplay) ? 1 : 0;
172
173 if (ids)
174 ids[x] = active[x];
175 if (bounds)
176 bounds[x] = rect;
177 }
178
179 return count;
180}
181
182static UINT32 mac_shadow_enum_monitors(MONITOR_DEF* monitors, UINT32 maxMonitors)
183{
184 return mac_shadow_list_displays(monitors, nullptr, nullptr, maxMonitors);
185}
186
187static BOOL mac_shadow_post(CGEventRef event)
188{
189 if (!event)
190 return FALSE;
191
192 CGEventPost(kCGHIDEventTap, event);
193 CFRelease(event);
194 return TRUE;
195}
196
197WINPR_ATTR_NODISCARD
198static BOOL mac_shadow_input_synchronize_event(rdpShadowSubsystem* subsystem,
199 rdpShadowClient* client, UINT32 flags)
200{
201 macShadowSubsystem* mac = (macShadowSubsystem*)subsystem;
202
203 if (!subsystem || !client)
204 return FALSE;
205
206 /* The client sends its lock keys when it connects and when it gets the
207 * focus back. Caps Lock is the only one a Mac has. The events posted from
208 * here carry mac->modifiers, so taking the client's state there makes the
209 * next key type the case the client shows. No event is posted for it: a
210 * Caps Lock event would also switch input sources on Macs set up that
211 * way. Num and Scroll Lock have no Mac state to follow. */
212 if (flags & KBD_SYNC_CAPS_LOCK)
213 mac->modifiers |= kCGEventFlagMaskAlphaShift;
214 else
215 mac->modifiers &= ~kCGEventFlagMaskAlphaShift;
216
217 WLog_DBG(TAG, "synchronize 0x%08" PRIx32 ": caps lock %s", flags,
218 (flags & KBD_SYNC_CAPS_LOCK) ? "on" : "off");
219 return TRUE;
220}
221
222WINPR_ATTR_NODISCARD
223static BOOL mac_shadow_input_keyboard_event(rdpShadowSubsystem* subsystem, rdpShadowClient* client,
224 UINT16 flags, UINT8 code)
225{
226 macShadowSubsystem* mac = (macShadowSubsystem*)subsystem;
227
228 if (!subsystem || !client)
229 return FALSE;
230
231 DWORD scancode = code;
232 if (flags & KBD_FLAGS_EXTENDED)
233 scancode |= KBDEXT;
234
235 /* WinPR's Apple table lists the extended keys (both Command keys, right
236 * Option, the arrows, Home, End, Page Up/Down, Forward Delete, keypad
237 * Enter, ...) with KBDEXT set, so the flag has to come along */
238 DWORD vkcode = GetVirtualKeyCodeFromVirtualScanCode(scancode, WINPR_KBD_TYPE_IBM_ENHANCED);
239 if (flags & KBD_FLAGS_EXTENDED)
240 vkcode |= KBDEXT;
241 const DWORD keycode = GetKeycodeFromVirtualKeyCode(vkcode, WINPR_KEYCODE_TYPE_APPLE);
242
243 /* Apple's key code 0 is A: any other key that maps to 0 has no Mac key */
244 if ((keycode == 0) && (vkcode != VK_KEY_A))
245 {
246 WLog_DBG(TAG, "no key for scancode 0x%04" PRIx32, scancode);
247 return TRUE;
248 }
249
250 /* A key is pressed unless the release flag is set; KBD_FLAGS_DOWN only
251 * says it was down already (a repeat). */
252 const BOOL down = (flags & KBD_FLAGS_RELEASE) ? FALSE : TRUE;
253
254 if ((keycode == MAC_VK_CAPSLOCK) && !down)
255 return TRUE;
256
257 CGEventRef event = CGEventCreateKeyboardEvent(nullptr, (CGKeyCode)keycode, down);
258 if (!event)
259 return FALSE;
260
261 /* Events posted from here do not move the system's modifier state, so the
262 * modifiers held are tracked and set on every event, and a modifier key
263 * itself becomes a flags-changed event, as from a real keyboard. */
264 const CGEventFlags modifier = mac_shadow_modifier_flag((CGKeyCode)keycode);
265 if (modifier)
266 {
267 if (down)
268 mac->modifiers |= modifier;
269 else
270 mac->modifiers &= ~modifier;
271 CGEventSetType(event, kCGEventFlagsChanged);
272 }
273 else if (keycode == MAC_VK_CAPSLOCK)
274 {
275 mac->modifiers ^= kCGEventFlagMaskAlphaShift;
276 CGEventSetType(event, kCGEventFlagsChanged);
277 }
278
279 CGEventSetFlags(event, mac->modifiers);
280 return mac_shadow_post(event);
281}
282
283WINPR_ATTR_NODISCARD
284static BOOL mac_shadow_input_unicode_keyboard_event(rdpShadowSubsystem* subsystem,
285 rdpShadowClient* client, UINT16 flags,
286 UINT16 code)
287{
288 if (!subsystem || !client)
289 return FALSE;
290
291 /* a key event with no key: the text it types is the character */
292 const BOOL down = (flags & KBD_FLAGS_RELEASE) ? FALSE : TRUE;
293 CGEventRef event = CGEventCreateKeyboardEvent(nullptr, 0, down);
294 if (!event)
295 return FALSE;
296
297 const UniChar character = code;
298 CGEventKeyboardSetUnicodeString(event, 1, &character);
299 return mac_shadow_post(event);
300}
301
302static CGPoint mac_shadow_point(const macShadowSubsystem* mac, UINT16 x, UINT16 y)
303{
304 const CGFloat scaleX = mac->bounds.size.width / (CGFloat)mac->width;
305 const CGFloat scaleY = mac->bounds.size.height / (CGFloat)mac->height;
306
307 return CGPointMake(mac->bounds.origin.x + (CGFloat)x * scaleX,
308 mac->bounds.origin.y + (CGFloat)y * scaleY);
309}
310
311static BOOL mac_shadow_mouse_event(macShadowSubsystem* mac, CGEventType type, CGPoint point,
312 CGMouseButton button, int64_t clicks)
313{
314 CGEventRef event = CGEventCreateMouseEvent(nullptr, type, point, button);
315 if (!event)
316 return FALSE;
317
318 if (clicks > 0)
319 CGEventSetIntegerValueField(event, kCGMouseEventClickState, clicks);
320 CGEventSetFlags(event, mac->modifiers);
321 return mac_shadow_post(event);
322}
323
324static BOOL mac_shadow_mouse_move(macShadowSubsystem* mac, CGPoint point)
325{
326 EnterCriticalSection(&mac->clientLock);
327 mac->pointerInput = GetTickCount64();
328 LeaveCriticalSection(&mac->clientLock);
329
330 /* with a button held the move is a drag, as macOS expects it */
331 if (mac->buttonsDown & (1u << kCGMouseButtonLeft))
332 return mac_shadow_mouse_event(mac, kCGEventLeftMouseDragged, point, kCGMouseButtonLeft, 0);
333 if (mac->buttonsDown & (1u << kCGMouseButtonRight))
334 return mac_shadow_mouse_event(mac, kCGEventRightMouseDragged, point, kCGMouseButtonRight,
335 0);
336 for (UINT32 button = kCGMouseButtonCenter; button < 32; button++)
337 {
338 if (mac->buttonsDown & (1u << button))
339 return mac_shadow_mouse_event(mac, kCGEventOtherMouseDragged, point,
340 (CGMouseButton)button, 0);
341 }
342 return mac_shadow_mouse_event(mac, kCGEventMouseMoved, point, kCGMouseButtonLeft, 0);
343}
344
345static BOOL mac_shadow_mouse_button(macShadowSubsystem* mac, CGMouseButton button, BOOL down,
346 CGPoint point)
347{
348 CGEventType type = kCGEventNull;
349
350 switch (button)
351 {
352 case kCGMouseButtonLeft:
353 type = down ? kCGEventLeftMouseDown : kCGEventLeftMouseUp;
354 break;
355 case kCGMouseButtonRight:
356 type = down ? kCGEventRightMouseDown : kCGEventRightMouseUp;
357 break;
358 default:
359 type = down ? kCGEventOtherMouseDown : kCGEventOtherMouseUp;
360 break;
361 }
362
363 /* macOS counts clicks from the click state of the event itself: without
364 * it a double click is two single clicks. */
365 if (down)
366 {
367 const UINT64 now = GetTickCount64();
368 if ((button == mac->lastClickButton) && (now - mac->lastClickTime <= MAC_SHADOW_CLICK_MS) &&
369 (fabs(point.x - mac->lastClickPoint.x) <= MAC_SHADOW_CLICK_DISTANCE) &&
370 (fabs(point.y - mac->lastClickPoint.y) <= MAC_SHADOW_CLICK_DISTANCE))
371 mac->clickCount++;
372 else
373 mac->clickCount = 1;
374
375 mac->lastClickTime = now;
376 mac->lastClickButton = button;
377 mac->lastClickPoint = point;
378 mac->buttonsDown |= (1u << button);
379 }
380 else
381 mac->buttonsDown &= ~(1u << button);
382
383 const int64_t clicks = (button == mac->lastClickButton) ? mac->clickCount : 1;
384 return mac_shadow_mouse_event(mac, type, point, button, clicks);
385}
386
387static BOOL mac_shadow_mouse_wheel(macShadowSubsystem* mac, UINT16 flags, BOOL horizontal)
388{
389 /* 9-bit two's complement, 120 for one notch */
390 INT32 delta = flags & WheelRotationMask;
391 if (flags & PTR_FLAGS_WHEEL_NEGATIVE)
392 delta -= 0x200;
393
394 INT32* remainder = &mac->wheelRemainder[horizontal ? 1 : 0];
395 *remainder += delta;
396 const INT32 lines = *remainder / 120;
397 if (lines == 0)
398 return TRUE;
399 *remainder -= lines * 120;
400
401 /* RDP: positive is up, and right; Quartz: positive is up, and left */
402 const int32_t vertical = horizontal ? 0 : lines;
403 const int32_t sideways = horizontal ? -lines : 0;
404 CGEventRef event =
405 CGEventCreateScrollWheelEvent(nullptr, kCGScrollEventUnitLine, 2, vertical, sideways);
406 if (!event)
407 return FALSE;
408
409 CGEventSetFlags(event, mac->modifiers);
410 return mac_shadow_post(event);
411}
412
413WINPR_ATTR_NODISCARD
414static BOOL mac_shadow_input_mouse_event(rdpShadowSubsystem* subsystem, rdpShadowClient* client,
415 UINT16 flags, UINT16 x, UINT16 y)
416{
417 macShadowSubsystem* mac = (macShadowSubsystem*)subsystem;
418
419 if (!subsystem || !client)
420 return FALSE;
421
422 if (flags & PTR_FLAGS_WHEEL)
423 return mac_shadow_mouse_wheel(mac, flags, FALSE);
424 if (flags & PTR_FLAGS_HWHEEL)
425 return mac_shadow_mouse_wheel(mac, flags, TRUE);
426
427 const CGPoint point = mac_shadow_point(mac, x, y);
428
429 if (flags & PTR_FLAGS_MOVE)
430 {
431 if (!mac_shadow_mouse_move(mac, point))
432 return FALSE;
433 }
434
435 const BOOL down = (flags & PTR_FLAGS_DOWN) ? TRUE : FALSE;
436 if (flags & PTR_FLAGS_BUTTON1)
437 return mac_shadow_mouse_button(mac, kCGMouseButtonLeft, down, point);
438 if (flags & PTR_FLAGS_BUTTON2)
439 return mac_shadow_mouse_button(mac, kCGMouseButtonRight, down, point);
440 if (flags & PTR_FLAGS_BUTTON3)
441 return mac_shadow_mouse_button(mac, kCGMouseButtonCenter, down, point);
442
443 return TRUE;
444}
445
446WINPR_ATTR_NODISCARD
447static BOOL mac_shadow_input_extended_mouse_event(rdpShadowSubsystem* subsystem,
448 rdpShadowClient* client, UINT16 flags, UINT16 x,
449 UINT16 y)
450{
451 macShadowSubsystem* mac = (macShadowSubsystem*)subsystem;
452
453 if (!subsystem || !client)
454 return FALSE;
455
456 const CGPoint point = mac_shadow_point(mac, x, y);
457
458 if (flags & PTR_FLAGS_MOVE)
459 {
460 if (!mac_shadow_mouse_move(mac, point))
461 return FALSE;
462 }
463
464 /* X1 and X2 are the fourth and fifth buttons, 3 and 4 counted from 0 */
465 const BOOL down = (flags & PTR_XFLAGS_DOWN) ? TRUE : FALSE;
466 if (flags & PTR_XFLAGS_BUTTON1)
467 return mac_shadow_mouse_button(mac, (CGMouseButton)3, down, point);
468 if (flags & PTR_XFLAGS_BUTTON2)
469 return mac_shadow_mouse_button(mac, (CGMouseButton)4, down, point);
470
471 return TRUE;
472}
473
474#ifdef WITH_PAM
475typedef struct
476{
477 const char* user;
478 const char* password;
479} MAC_SHADOW_PAM_DATA;
480
481static int mac_shadow_pam_conv(int num_msg, const struct pam_message** msg,
482 struct pam_response** resp, void* appdata_ptr)
483{
484 const MAC_SHADOW_PAM_DATA* data = (const MAC_SHADOW_PAM_DATA*)appdata_ptr;
485
486 if ((num_msg <= 0) || !msg || !resp || !data)
487 return PAM_CONV_ERR;
488
489 struct pam_response* response = calloc((size_t)num_msg, sizeof(struct pam_response));
490 if (!response)
491 return PAM_BUF_ERR;
492
493 for (int index = 0; index < num_msg; index++)
494 {
495 const char* answer = nullptr;
496 switch (msg[index]->msg_style)
497 {
498 case PAM_PROMPT_ECHO_ON:
499 answer = data->user;
500 break;
501 case PAM_PROMPT_ECHO_OFF:
502 answer = data->password;
503 break;
504 default:
505 continue;
506 }
507
508 response[index].resp = _strdup(answer);
509 if (!response[index].resp)
510 {
511 for (int x = 0; x < index; x++)
512 free(response[x].resp);
513 free(response);
514 return PAM_BUF_ERR;
515 }
516 }
517
518 *resp = response;
519 return PAM_SUCCESS;
520}
521
522/* The screen shared is the one of the user running the server: only that
523 * user may log in, with the password of their account (checked by Open
524 * Directory, through PAM's "checkpw" service). */
525WINPR_ATTR_NODISCARD
526static int mac_shadow_pam_authenticate(WINPR_ATTR_UNUSED rdpShadowSubsystem* subsystem,
527 WINPR_ATTR_UNUSED rdpShadowClient* client, const char* user,
528 WINPR_ATTR_UNUSED const char* domain, const char* password)
529{
530 if (!user || !password)
531 return -1;
532
533 const struct passwd* owner = getpwuid(getuid());
534 if (!owner || !owner->pw_name || (strcmp(owner->pw_name, user) != 0))
535 {
536 WLog_ERR(TAG, "login as '%s' refused: this screen belongs to '%s'", user,
537 (owner && owner->pw_name) ? owner->pw_name : "?");
538 return -1;
539 }
540
541 MAC_SHADOW_PAM_DATA data = { .user = user, .password = password };
542 const struct pam_conv conv = { .conv = mac_shadow_pam_conv, .appdata_ptr = &data };
543 pam_handle_t* handle = nullptr;
544
545 int status = pam_start("checkpw", user, &conv, &handle);
546 if (status != PAM_SUCCESS)
547 {
548 WLog_ERR(TAG, "pam_start failure: %s", pam_strerror(handle, status));
549 return -1;
550 }
551
552 /* checkpw takes the password as given (use_first_pass), it does not ask */
553 status = pam_set_item(handle, PAM_AUTHTOK, password);
554 if (status == PAM_SUCCESS)
555 status = pam_authenticate(handle, 0);
556 if (status == PAM_SUCCESS)
557 status = pam_acct_mgmt(handle, 0);
558
559 if (status != PAM_SUCCESS)
560 WLog_ERR(TAG, "login as '%s' failed: %s", user, pam_strerror(handle, status));
561
562 pam_end(handle, status);
563 return (status == PAM_SUCCESS) ? 1 : -1;
564}
565#endif
566
567static void mac_shadow_on_frame(void* context, CVPixelBufferRef frame, const CGRect* dirty,
568 size_t count)
569{
570 macShadowSubsystem* mac = (macShadowSubsystem*)context;
571 WINPR_ASSERT(mac);
572
573 const CGRect frameRect = CGRectMake(0, 0, (CGFloat)CVPixelBufferGetWidth(frame),
574 (CGFloat)CVPixelBufferGetHeight(frame));
575
576 EnterCriticalSection(&mac->frameLock);
577
578 /* keep the newest frame only: what changed in the ones skipped is still
579 * in the pending region, and the newest frame has it */
580 CVPixelBufferRetain(frame);
581 if (mac->frame)
582 CVPixelBufferRelease(mac->frame);
583 mac->frame = frame;
584
585 for (size_t x = 0; x < count; x++)
586 {
587 const CGRect rect = CGRectIntegral(CGRectIntersection(dirty[x], frameRect));
588 if (CGRectIsEmpty(rect))
589 continue;
590
591 const RECTANGLE_16 invalid = { .left = (UINT16)rect.origin.x,
592 .top = (UINT16)rect.origin.y,
593 .right = (UINT16)(rect.origin.x + rect.size.width),
594 .bottom = (UINT16)(rect.origin.y + rect.size.height) };
595 (void)region16_union_rect(&mac->pending, &mac->pending, &invalid);
596 }
597
598 LeaveCriticalSection(&mac->frameLock);
599 (void)SetEvent(mac->frameEvent);
600}
601
602static void mac_shadow_on_stopped(void* context)
603{
604 macShadowSubsystem* mac = (macShadowSubsystem*)context;
605 WINPR_ASSERT(mac);
606
607 /* the subsystem thread makes a new stream once the display is back */
608 (void)SetEvent(mac->lostEvent);
609}
610
611static void mac_shadow_audio_msg_free(WINPR_ATTR_UNUSED UINT32 id, SHADOW_MSG_OUT* msg)
612{
614 if (samples)
615 free(samples->buf);
616 free(samples);
617}
618
619/* Sends what s holds to every client, as one message */
620static void mac_shadow_audio_post(macShadowSubsystem* mac, wStream* s)
621{
622 const size_t size = Stream_GetPosition(s);
623 Stream_ResetPosition(s);
624 if (size == 0)
625 return;
626
627 rdpShadowServer* server = mac->common.server;
628 if (!server || !server->clients || (ArrayList_Count(server->clients) == 0))
629 return;
630
632 if (!msg)
633 return;
634
635 msg->buf = malloc(size);
636 if (!msg->buf)
637 {
638 free(msg);
639 return;
640 }
641
642 memcpy(msg->buf, Stream_Buffer(s), size);
643 mac->audioMessages++;
644 msg->common.Free = mac_shadow_audio_msg_free;
645 msg->audio_format = &mac_audio_source_format;
646 msg->nFrames = size / MAC_AUDIO_FRAME_BYTES;
647 msg->wTimestamp = (UINT16)(GetTickCount64() & 0xFFFF);
648 /* the message is freed with its last reference, also when no client took it */
649 (void)shadow_client_boardcast_msg(server, nullptr, SHADOW_MSG_OUT_AUDIO_OUT_SAMPLES_ID,
650 &msg->common, nullptr);
651}
652
653static INT16 mac_shadow_audio_sample(float value)
654{
655 if (value >= 1.0f)
656 return INT16_MAX;
657 if (value <= -1.0f)
658 return -INT16_MAX;
659 return (INT16)lrintf(value * (float)INT16_MAX);
660}
661
662static void mac_shadow_on_audio(void* context, const float* samples, size_t frames, UINT32 channels,
663 UINT32 rate)
664{
665 macShadowSubsystem* mac = (macShadowSubsystem*)context;
666 WINPR_ASSERT(mac);
667
668 if ((channels != MAC_AUDIO_CHANNELS) || (rate != MAC_AUDIO_RATE))
669 {
670 WLog_DBG(TAG, "sound in %" PRIu32 " Hz, %" PRIu32 " channels, not what was asked: dropped",
671 rate, channels);
672 return;
673 }
674
675 const UINT64 now = GetTickCount64();
676 if (mac->audioLogged == 0)
677 {
678 WLog_INFO(TAG, "sound: capturing %" PRIu32 " Hz, %" PRIu32 " channels", rate, channels);
679 mac->audioLogged = now;
680 }
681 else if (now - mac->audioLogged >= MAC_SHADOW_AUDIO_LOG_MS)
682 {
683 WLog_DBG(TAG,
684 "sound: %" PRIu64 " frames captured, %" PRIu64
685 " messages sent, peak %.3f in the last %" PRIu64 " ms",
686 mac->audioFrames, mac->audioMessages, (double)mac->audioPeak,
687 now - mac->audioLogged);
688 mac->audioFrames = 0;
689 mac->audioMessages = 0;
690 mac->audioPeak = 0.0f;
691 mac->audioLogged = now;
692 }
693 mac->audioFrames += frames;
694
695 wStream* s = mac->audioStream;
696 for (size_t f = 0; f < frames; f++)
697 {
698 for (UINT32 c = 0; c < channels; c++)
699 {
700 const float value = samples[f * channels + c];
701 mac->audioPeak = MAX(mac->audioPeak, fabsf(value));
702 Stream_Write_INT16(s, mac_shadow_audio_sample(value));
703 }
704
705 if (Stream_GetPosition(s) >= MAC_AUDIO_MESSAGE_FRAMES * MAC_AUDIO_FRAME_BYTES)
706 mac_shadow_audio_post(mac, s);
707 }
708}
709
710static BOOL mac_shadow_audio_init(macShadowSubsystem* mac)
711{
712 AUDIO_FORMAT* formats = audio_formats_new(ARRAYSIZE(mac_audio_offered_formats));
713 if (!formats)
714 return FALSE;
715
716 size_t count = 0;
717 for (size_t x = 0; x < ARRAYSIZE(mac_audio_offered_formats); x++)
718 {
719 const AUDIO_FORMAT* format = &mac_audio_offered_formats[x];
720 if (freerdp_dsp_supports_format(format, TRUE))
721 formats[count++] = *format;
722 }
723
724 mac->audioStream = Stream_New(nullptr, MAC_AUDIO_MESSAGE_FRAMES * MAC_AUDIO_FRAME_BYTES);
725 if (!mac->audioStream || (count == 0))
726 {
727 Stream_Free(mac->audioStream, TRUE);
728 mac->audioStream = nullptr;
729 audio_formats_free(formats, ARRAYSIZE(mac_audio_offered_formats));
730 return FALSE;
731 }
732
733 mac->common.rdpsndFormats = formats;
734 mac->common.nRdpsndFormats = count;
735 return TRUE;
736}
737
738static void mac_shadow_audio_uninit(macShadowSubsystem* mac)
739{
740 audio_formats_free(mac->common.rdpsndFormats, mac->common.nRdpsndFormats);
741 mac->common.rdpsndFormats = nullptr;
742 mac->common.nRdpsndFormats = 0;
743 Stream_Free(mac->audioStream, TRUE);
744 mac->audioStream = nullptr;
745}
746
747/* Copies what changed from the newest frame to the surface and lets the
748 * clients send it */
749static void mac_shadow_send_frame(macShadowSubsystem* mac)
750{
751 rdpShadowServer* server = mac->common.server;
752 rdpShadowSurface* surface = server->surface;
753 REGION16 region;
754 region16_init(&region);
755
756 EnterCriticalSection(&mac->frameLock);
757 CVPixelBufferRef frame = mac->frame;
758 if (frame)
759 CVPixelBufferRetain(frame);
760 (void)region16_copy(&region, &mac->pending);
761 region16_clear(&mac->pending);
762 LeaveCriticalSection(&mac->frameLock);
763
764 if (!frame)
765 goto out;
766
767 if ((CVPixelBufferGetWidth(frame) != surface->width) ||
768 (CVPixelBufferGetHeight(frame) != surface->height))
769 {
770 /* the display mode changed: the frames fit again once the surface has followed it */
771 if (!mac->sizeWarned)
772 WLog_DBG(TAG, "frames of %zux%zu for a surface of %" PRIu32 "x%" PRIu32 ": dropped",
773 CVPixelBufferGetWidth(frame), CVPixelBufferGetHeight(frame),
774 (UINT32)surface->width, (UINT32)surface->height);
775 mac->sizeWarned = TRUE;
776 goto out;
777 }
778
779 {
780 const RECTANGLE_16 surfaceRect = { 0, 0, (UINT16)surface->width, (UINT16)surface->height };
781 (void)region16_intersect_rect(&region, &region, &surfaceRect);
782 }
783
784 if (region16_is_empty(&region))
785 goto out;
786
787 if (CVPixelBufferLockBaseAddress(frame, kCVPixelBufferLock_ReadOnly) != kCVReturnSuccess)
788 goto out;
789
790 {
791 const BYTE* data = CVPixelBufferGetBaseAddress(frame);
792 const size_t stride = CVPixelBufferGetBytesPerRow(frame);
793 UINT32 count = 0;
794 const RECTANGLE_16* rects = region16_rects(&region, &count);
795
796 EnterCriticalSection(&surface->lock);
797 for (UINT32 x = 0; x < count; x++)
798 {
799 const RECTANGLE_16* rect = &rects[x];
800 (void)freerdp_image_copy_no_overlap(
801 surface->data, surface->format, surface->scanline, rect->left, rect->top,
802 rect->right - rect->left, rect->bottom - rect->top, data, PIXEL_FORMAT_BGRX32,
803 (UINT32)stride, rect->left, rect->top, nullptr, FREERDP_FLIP_NONE);
804 (void)region16_union_rect(&surface->invalidRegion, &surface->invalidRegion, rect);
805 }
806 LeaveCriticalSection(&surface->lock);
807 }
808
809 (void)CVPixelBufferUnlockBaseAddress(frame, kCVPixelBufferLock_ReadOnly);
810
811 ArrayList_Lock(server->clients);
812 if (ArrayList_Count(server->clients) > 0)
813 shadow_subsystem_frame_update(&mac->common);
814 ArrayList_Unlock(server->clients);
815
816 EnterCriticalSection(&surface->lock);
817 region16_clear(&surface->invalidRegion);
818 LeaveCriticalSection(&surface->lock);
819
820out:
821 if (frame)
822 CVPixelBufferRelease(frame);
823 region16_uninit(&region);
824}
825
826static void mac_shadow_refresh(macShadowSubsystem* mac)
827{
828 const RECTANGLE_16 all = { 0, 0, (UINT16)mac->width, (UINT16)mac->height };
829
830 EnterCriticalSection(&mac->frameLock);
831 (void)region16_union_rect(&mac->pending, &mac->pending, &all);
832 LeaveCriticalSection(&mac->frameLock);
833
834 EnterCriticalSection(&mac->clientLock);
835 mac->cursorResend = TRUE;
836 LeaveCriticalSection(&mac->clientLock);
837
838 (void)SetEvent(mac->frameEvent);
839}
840
841static void mac_shadow_cursor_msg_free(UINT32 id, SHADOW_MSG_OUT* msg)
842{
843 if (msg && (id == SHADOW_MSG_OUT_POINTER_ALPHA_UPDATE_ID))
844 {
846 free(shape->xorMaskData);
847 free(shape->andMaskData);
848 }
849 free(msg);
850}
851
852static UINT32 mac_shadow_cursor_hash(const macCursorImage* image)
853{
854 /* FNV-1a over the size, the hot spot and the pixels */
855 UINT32 hash = 2166136261u;
856 const UINT32 header[] = { image->width, image->height, image->xHot, image->yHot };
857 const BYTE* parts[] = { (const BYTE*)header, image->pixels };
858 const size_t sizes[] = { sizeof(header), 4ull * image->width * image->height };
859 for (size_t p = 0; p < ARRAYSIZE(parts); p++)
860 {
861 for (size_t x = 0; x < sizes[p]; x++)
862 {
863 hash ^= parts[p][x];
864 hash *= 16777619u;
865 }
866 }
867 return hash;
868}
869
870static BOOL mac_shadow_cursor_send_shape(macShadowSubsystem* mac, const macCursorImage* image)
871{
873 calloc(1, sizeof(SHADOW_MSG_OUT_POINTER_ALPHA_UPDATE));
874 if (!msg)
875 return FALSE;
876
877 msg->xHot = image->xHot;
878 msg->yHot = image->yHot;
879 msg->width = image->width;
880 msg->height = image->height;
881 if (shadow_subsystem_pointer_convert_alpha_pointer_data_to_format(
882 image->pixels, PIXEL_FORMAT_BGRA32, TRUE, image->width, image->height, msg) < 0)
883 {
884 free(msg);
885 return FALSE;
886 }
887
888 msg->common.Free = mac_shadow_cursor_msg_free;
889 return shadow_client_boardcast_msg(mac->common.server, nullptr,
890 SHADOW_MSG_OUT_POINTER_ALPHA_UPDATE_ID, &msg->common,
891 nullptr) >= 0;
892}
893
894static void mac_shadow_cursor_send_position(macShadowSubsystem* mac, UINT64 now)
895{
896 EnterCriticalSection(&mac->clientLock);
897 const UINT64 input = mac->pointerInput;
898 LeaveCriticalSection(&mac->clientLock);
899 if (now - input < MAC_SHADOW_CURSOR_QUIET_MS)
900 return;
901
902 CGEventRef event = CGEventCreate(nullptr);
903 if (!event)
904 return;
905 const CGPoint where = CGEventGetLocation(event);
906 CFRelease(event);
907
908 /* from global points to the surface, as mac_shadow_point the other way */
909 const CGFloat x =
910 (where.x - mac->bounds.origin.x) * (CGFloat)mac->width / mac->bounds.size.width;
911 const CGFloat y =
912 (where.y - mac->bounds.origin.y) * (CGFloat)mac->height / mac->bounds.size.height;
913 const UINT32 xPos = (UINT32)MIN(MAX(x, 0.0), (CGFloat)mac->width - 1.0);
914 const UINT32 yPos = (UINT32)MIN(MAX(y, 0.0), (CGFloat)mac->height - 1.0);
915 if ((xPos == mac->common.pointerX) && (yPos == mac->common.pointerY))
916 return;
917
918 mac->common.pointerX = xPos;
919 mac->common.pointerY = yPos;
920
923 if (!msg)
924 return;
925 msg->xPos = xPos;
926 msg->yPos = yPos;
927 msg->common.Free = mac_shadow_cursor_msg_free;
928 (void)shadow_client_boardcast_msg(mac->common.server, nullptr,
929 SHADOW_MSG_OUT_POINTER_POSITION_UPDATE_ID, &msg->common,
930 nullptr);
931}
932
933/* The clients draw the cursor themselves (the frames have none): its shape
934 * when it changed, and where it is when something else than a client moved it */
935static void mac_shadow_cursor_update(macShadowSubsystem* mac, UINT64 now)
936{
937 if (now < mac->cursorNext)
938 return;
939 mac->cursorNext = now + MAC_SHADOW_CURSOR_MS;
940
941 macCursorImage image = WINPR_C_ARRAY_INIT;
942 if (mac_cursor_get(&image, MAC_SHADOW_CURSOR_MAX))
943 {
944 EnterCriticalSection(&mac->clientLock);
945 const BOOL resend = mac->cursorResend;
946 mac->cursorResend = FALSE;
947 LeaveCriticalSection(&mac->clientLock);
948
949 const UINT32 hash = mac_shadow_cursor_hash(&image);
950 if ((resend || !mac->cursorKnown || (hash != mac->cursorHash)) &&
951 mac_shadow_cursor_send_shape(mac, &image))
952 {
953 mac->cursorHash = hash;
954 mac->cursorKnown = TRUE;
955 }
956 mac_cursor_image_free(&image);
957 }
958
959 mac_shadow_cursor_send_position(mac, now);
960}
961
962static BOOL mac_shadow_has_clients(macShadowSubsystem* mac)
963{
964 EnterCriticalSection(&mac->clientLock);
965 const BOOL clients = mac->clients > 0;
966 LeaveCriticalSection(&mac->clientLock);
967 return clients;
968}
969
970static void mac_shadow_subsystem_process_message(macShadowSubsystem* mac, wMessage* message)
971{
972 switch (message->id)
973 {
974 case SHADOW_MSG_IN_REFRESH_REQUEST_ID:
975 mac_shadow_refresh(mac);
976 break;
977
978 default:
979 WLog_ERR(TAG, "Unknown message id: %" PRIu32 "", message->id);
980 break;
981 }
982
983 if (message->Free)
984 message->Free(message);
985}
986
987/* Replaces the stream that ended with a new one. It fails while there is no
988 * display to capture (the display is off, the lid is closed). */
989static BOOL mac_shadow_capture_restart(macShadowSubsystem* mac)
990{
991 /* ask Quartz first: ScreenCaptureKit would log an error on every try */
992 if (!CGDisplayIsActive(mac->display) || CGDisplayIsAsleep(mac->display))
993 return FALSE;
994
995 mac_capture_free(mac->capture);
996 mac->capture = mac_capture_new(&mac->config);
997 if (!mac->capture)
998 return FALSE;
999
1000 if (!mac_capture_start(mac->capture))
1001 {
1002 mac_capture_free(mac->capture);
1003 mac->capture = nullptr;
1004 return FALSE;
1005 }
1006
1007 /* the clients get the whole screen again with the first frame */
1008 mac_shadow_refresh(mac);
1009 return TRUE;
1010}
1011
1012/* the virtual screen spans every monitor (they start at 0, see mac_shadow_list_displays) */
1013static void mac_shadow_set_virtual_screen(rdpShadowSubsystem* subsystem, UINT32 count)
1014{
1015 WINPR_ASSERT(subsystem);
1016
1017 MONITOR_DEF* virtualScreen = &subsystem->virtualScreen;
1018 virtualScreen->left = 0;
1019 virtualScreen->top = 0;
1020 virtualScreen->right = 0;
1021 virtualScreen->bottom = 0;
1022 for (UINT32 x = 0; x < count; x++)
1023 {
1024 virtualScreen->right = MAX(virtualScreen->right, subsystem->monitors[x].right);
1025 virtualScreen->bottom = MAX(virtualScreen->bottom, subsystem->monitors[x].bottom);
1026 }
1027 virtualScreen->flags = 1;
1028}
1029
1030/* The displays changed: the surface takes the new size of the display shared,
1031 * the clients get a desktop resize from the server, and the capture starts
1032 * again at the new size. */
1033static void mac_shadow_displays_changed(macShadowSubsystem* mac, BOOL* lost)
1034{
1035 WINPR_ASSERT(mac);
1036 WINPR_ASSERT(lost);
1037
1038 rdpShadowSubsystem* subsystem = &mac->common;
1039 MONITOR_DEF monitors[MAC_SHADOW_MAX_DISPLAYS] = WINPR_C_ARRAY_INIT;
1040 CGDirectDisplayID ids[MAC_SHADOW_MAX_DISPLAYS] = WINPR_C_ARRAY_INIT;
1041 CGRect bounds[MAC_SHADOW_MAX_DISPLAYS] = WINPR_C_ARRAY_INIT;
1042
1043 const UINT32 count = mac_shadow_list_displays(monitors, ids, bounds, MAC_SHADOW_MAX_DISPLAYS);
1044 if (count == 0)
1045 return;
1046
1047 UINT32 selected = 0;
1048 for (UINT32 x = 0; x < count; x++)
1049 {
1050 if (ids[x] == mac->display)
1051 selected = x; /* the same display if it is still there */
1052 }
1053 const MONITOR_DEF* monitor = &monitors[selected];
1054 const UINT32 width = WINPR_ASSERTING_INT_CAST(UINT32, monitor->right - monitor->left + 1);
1055 const UINT32 height = WINPR_ASSERTING_INT_CAST(UINT32, monitor->bottom - monitor->top + 1);
1056
1057 WLog_INFO(TAG,
1058 "the displays changed: sharing display %" PRIu32 " (%" PRIu32 " of %" PRIu32
1059 "), %" PRIu32 "x%" PRIu32 " (was %" PRIu32 "x%" PRIu32 ")",
1060 ids[selected], selected, count, width, height, mac->width, mac->height);
1061
1062 mac_capture_stop(mac->capture);
1063 mac_capture_free(mac->capture);
1064 mac->capture = nullptr;
1065
1066 memcpy(subsystem->monitors, monitors, sizeof(MONITOR_DEF) * count);
1067 subsystem->numMonitors = count;
1068 subsystem->selectedMonitor = selected;
1069 mac_shadow_set_virtual_screen(subsystem, count);
1070 mac->display = ids[selected];
1071 mac->bounds = bounds[selected];
1072 mac->width = width;
1073 mac->height = height;
1074 mac->config.display = mac->display;
1075 mac->config.width = width;
1076 mac->config.height = height;
1077
1078 /* a frame of the old size is no use any more */
1079 EnterCriticalSection(&mac->frameLock);
1080 if (mac->frame)
1081 CVPixelBufferRelease(mac->frame);
1082 mac->frame = nullptr;
1083 region16_clear(&mac->pending);
1084 LeaveCriticalSection(&mac->frameLock);
1085 mac->sizeWarned = FALSE;
1086
1087 if (!shadow_screen_resize(subsystem->server->screen))
1088 WLog_ERR(TAG, "the surface could not take the new size");
1089
1090 /* tried again every second if this fails */
1091 *lost = !mac_shadow_capture_restart(mac);
1092}
1093
1094static void mac_shadow_check_displays(macShadowSubsystem* mac, UINT64 now, BOOL* lost)
1095{
1096 WINPR_ASSERT(mac);
1097 WINPR_ASSERT(lost);
1098
1099 if (now < mac->displaysChecked + MAC_SHADOW_DISPLAYS_MS)
1100 return;
1101 mac->displaysChecked = now;
1102
1103 uint32_t count = 0;
1104 const CGRect bounds = CGDisplayBounds(mac->display);
1105 if ((CGGetActiveDisplayList(0, nullptr, &count) != kCGErrorSuccess) ||
1106 ((count == mac->common.numMonitors) && CGRectEqualToRect(bounds, mac->bounds)))
1107 return;
1108
1109 mac_shadow_displays_changed(mac, lost);
1110}
1111
1112static DWORD WINAPI mac_shadow_subsystem_thread(LPVOID arg)
1113{
1114 macShadowSubsystem* mac = (macShadowSubsystem*)arg;
1115 wMessagePipe* MsgPipe = mac->common.MsgPipe;
1116 HANDLE events[] = { mac->stopEvent, MessageQueue_Event(MsgPipe->In), mac->frameEvent,
1117 mac->lostEvent };
1118 BOOL lost = FALSE;
1119 UINT64 restart = 0;
1120
1121 while (1)
1122 {
1123 /* with clients, look at the cursor; without a stream, try to make one
1124 * again every second; in any case look at the displays every second */
1125 const BOOL clients = mac_shadow_has_clients(mac);
1126 DWORD timeout = clients ? MAC_SHADOW_CURSOR_MS : MAC_SHADOW_DISPLAYS_MS;
1127 if (lost)
1128 timeout = MIN(timeout, MAC_SHADOW_RESTART_MS);
1129
1130 const DWORD status = WaitForMultipleObjects(ARRAYSIZE(events), events, FALSE, timeout);
1131 if ((status == WAIT_FAILED) || (WaitForSingleObject(mac->stopEvent, 0) == WAIT_OBJECT_0))
1132 break;
1133
1134 const UINT64 now = GetTickCount64();
1135 if (WaitForSingleObject(mac->lostEvent, 0) == WAIT_OBJECT_0)
1136 {
1137 (void)ResetEvent(mac->lostEvent);
1138 if (!lost)
1139 WLog_WARN(TAG, "the screen is no longer captured (the display is off?): the "
1140 "clients see the last picture until it is back");
1141 lost = TRUE;
1142 restart = now + MAC_SHADOW_RESTART_MS;
1143 }
1144 else if (lost && (now >= restart))
1145 {
1146 restart = now + MAC_SHADOW_RESTART_MS;
1147 if (mac_shadow_capture_restart(mac))
1148 {
1149 WLog_INFO(TAG, "the display is back: capturing the screen again");
1150 lost = FALSE;
1151 }
1152 }
1153
1154 mac_shadow_check_displays(mac, now, &lost);
1155 if (clients)
1156 mac_shadow_cursor_update(mac, now);
1157
1158 wMessage message = WINPR_C_ARRAY_INIT;
1159 while (MessageQueue_Peek(MsgPipe->In, &message, TRUE))
1160 {
1161 if (message.id == WMQ_QUIT)
1162 goto out;
1163 mac_shadow_subsystem_process_message(mac, &message);
1164 }
1165
1166 if (WaitForSingleObject(mac->frameEvent, 0) == WAIT_OBJECT_0)
1167 {
1168 (void)ResetEvent(mac->frameEvent);
1169 mac_shadow_send_frame(mac);
1170 }
1171 }
1172
1173out:
1174 ExitThread(0);
1175 return 0;
1176}
1177
1178static BOOL mac_shadow_check_input_access(void)
1179{
1180 /* posting input needs the Accessibility permission; without it the
1181 * clients only watch */
1182 const void* keys[] = { kAXTrustedCheckOptionPrompt };
1183 const void* values[] = { kCFBooleanTrue };
1184 CFDictionaryRef options =
1185 CFDictionaryCreate(kCFAllocatorDefault, keys, values, ARRAYSIZE(keys),
1186 &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
1187 const Boolean trusted = AXIsProcessTrustedWithOptions(options);
1188 if (options)
1189 CFRelease(options);
1190
1191 if (!trusted)
1192 WLog_WARN(TAG, "no permission to control the computer: the clients can only watch. "
1193 "Allow this program in System Settings > Privacy & Security > "
1194 "Accessibility, then start it again");
1195 return trusted ? TRUE : FALSE;
1196}
1197
1198WINPR_ATTR_NODISCARD
1199static int mac_shadow_subsystem_init(rdpShadowSubsystem* subsystem)
1200{
1201 macShadowSubsystem* mac = (macShadowSubsystem*)subsystem;
1202 CGDirectDisplayID ids[MAC_SHADOW_MAX_DISPLAYS] = WINPR_C_ARRAY_INIT;
1203 CGRect bounds[MAC_SHADOW_MAX_DISPLAYS] = WINPR_C_ARRAY_INIT;
1204
1205 const UINT32 count = mac_shadow_list_displays(subsystem->monitors, ids, bounds, ARRAYSIZE(ids));
1206 if (count == 0)
1207 {
1208 WLog_ERR(TAG, "no display found");
1209 return -1;
1210 }
1211
1212 subsystem->numMonitors = count;
1213 if (subsystem->selectedMonitor >= count)
1214 subsystem->selectedMonitor = 0;
1215
1216 const MONITOR_DEF* monitor = &subsystem->monitors[subsystem->selectedMonitor];
1217 mac->display = ids[subsystem->selectedMonitor];
1218 mac->bounds = bounds[subsystem->selectedMonitor];
1219 /* the surface has the size of the display in points: on a Retina display
1220 * ScreenCaptureKit scales the picture down to it */
1221 mac->width = (UINT32)(monitor->right - monitor->left + 1);
1222 mac->height = (UINT32)(monitor->bottom - monitor->top + 1);
1223
1224 mac_shadow_set_virtual_screen(subsystem, count);
1225
1226 /* the frames never carry the cursor (ScreenCaptureKit leaves it out): the
1227 * clients draw it, from the shapes the server sends. Without this the
1228 * server sends them the default arrow instead, ShowMouseCursor being
1229 * otherwise set only by /server-side-cursor */
1230 if (subsystem->server)
1231 subsystem->server->ShowMouseCursor = TRUE;
1232
1233 WLog_INFO(TAG,
1234 "sharing display %" PRIu32 " (%" PRIu32 " of %" PRIu32 "): %" PRIu32 "x%" PRIu32
1235 " at %.0f,%.0f",
1236 mac->display, subsystem->selectedMonitor, count, mac->width, mac->height,
1237 mac->bounds.origin.x, mac->bounds.origin.y);
1238
1239 if (!mac_capture_check_access())
1240 return -1;
1241
1242 (void)mac_shadow_check_input_access();
1243
1244 const BOOL audio = mac_shadow_audio_init(mac);
1245 if (!audio)
1246 WLog_WARN(TAG, "sound: no format to offer: no sound");
1247
1248 const macCaptureConfig config = { .display = mac->display,
1249 .width = mac->width,
1250 .height = mac->height,
1251 .fps = MAC_SHADOW_FPS,
1252 /* the clients draw the cursor */
1253 .showCursor = FALSE,
1254 .audio = audio,
1255 .audioRate = MAC_AUDIO_RATE,
1256 .audioChannels = MAC_AUDIO_CHANNELS,
1257 .onFrame = mac_shadow_on_frame,
1258 .onAudio = audio ? mac_shadow_on_audio : nullptr,
1259 .onStopped = mac_shadow_on_stopped,
1260 .context = mac };
1261 mac->config = config;
1262
1263 mac->capture = mac_capture_new(&mac->config);
1264 if (!mac->capture)
1265 return -1;
1266
1267 return 1;
1268}
1269
1270static int mac_shadow_subsystem_uninit(rdpShadowSubsystem* subsystem)
1271{
1272 macShadowSubsystem* mac = (macShadowSubsystem*)subsystem;
1273 if (!mac)
1274 return -1;
1275
1276 mac_capture_free(mac->capture);
1277 mac->capture = nullptr;
1278
1279 EnterCriticalSection(&mac->frameLock);
1280 if (mac->frame)
1281 CVPixelBufferRelease(mac->frame);
1282 mac->frame = nullptr;
1283 region16_clear(&mac->pending);
1284 LeaveCriticalSection(&mac->frameLock);
1285
1286 mac_shadow_audio_uninit(mac);
1287 return 1;
1288}
1289
1290static int mac_shadow_subsystem_stop(rdpShadowSubsystem* subsystem)
1291{
1292 macShadowSubsystem* mac = (macShadowSubsystem*)subsystem;
1293
1294 if (!mac)
1295 return -1;
1296
1297 /* the thread first: it may be replacing the stream */
1298 if (mac->thread)
1299 {
1300 (void)SetEvent(mac->stopEvent);
1301 (void)WaitForSingleObject(mac->thread, INFINITE);
1302 (void)CloseHandle(mac->thread);
1303 mac->thread = nullptr;
1304 }
1305
1306 mac_capture_stop(mac->capture);
1307 return 1;
1308}
1309
1310/* A connected client keeps the display awake: when it turns off there is
1311 * nothing left to capture. The first client also wakes it, if it slept. */
1312WINPR_ATTR_NODISCARD
1313static BOOL mac_shadow_client_connect(rdpShadowSubsystem* subsystem,
1314 WINPR_ATTR_UNUSED rdpShadowClient* client)
1315{
1316 macShadowSubsystem* mac = (macShadowSubsystem*)subsystem;
1317 WINPR_ASSERT(mac);
1318
1319 EnterCriticalSection(&mac->clientLock);
1320 if (mac->clients++ == 0)
1321 {
1322 IOPMAssertionID activity = kIOPMNullAssertionID;
1323 if (IOPMAssertionDeclareUserActivity(CFSTR("FreeRDP shadow server: a client connected"),
1324 kIOPMUserActiveRemote, &activity) == kIOReturnSuccess)
1325 (void)IOPMAssertionRelease(activity);
1326
1327 if (IOPMAssertionCreateWithName(kIOPMAssertionTypePreventUserIdleDisplaySleep,
1328 kIOPMAssertionLevelOn,
1329 CFSTR("FreeRDP shadow server: clients are connected"),
1330 &mac->awake) != kIOReturnSuccess)
1331 {
1332 WLog_WARN(TAG, "could not keep the display awake: when it turns off, the clients "
1333 "see the last picture");
1334 mac->awake = kIOPMNullAssertionID;
1335 }
1336 }
1337 LeaveCriticalSection(&mac->clientLock);
1338 return TRUE;
1339}
1340
1341static void mac_shadow_client_disconnect(rdpShadowSubsystem* subsystem,
1342 WINPR_ATTR_UNUSED rdpShadowClient* client)
1343{
1344 macShadowSubsystem* mac = (macShadowSubsystem*)subsystem;
1345 WINPR_ASSERT(mac);
1346
1347 EnterCriticalSection(&mac->clientLock);
1348 if ((mac->clients > 0) && (--mac->clients == 0) && (mac->awake != kIOPMNullAssertionID))
1349 {
1350 (void)IOPMAssertionRelease(mac->awake);
1351 mac->awake = kIOPMNullAssertionID;
1352 }
1353 LeaveCriticalSection(&mac->clientLock);
1354}
1355
1356WINPR_ATTR_NODISCARD
1357static int mac_shadow_subsystem_start(rdpShadowSubsystem* subsystem)
1358{
1359 macShadowSubsystem* mac = (macShadowSubsystem*)subsystem;
1360
1361 if (!mac)
1362 return -1;
1363
1364 (void)ResetEvent(mac->stopEvent);
1365 mac->thread = CreateThread(nullptr, 0, mac_shadow_subsystem_thread, mac, 0, nullptr);
1366 if (!mac->thread)
1367 {
1368 WLog_ERR(TAG, "Failed to create thread");
1369 return -1;
1370 }
1371
1372 if (!mac_capture_start(mac->capture))
1373 {
1374 (void)mac_shadow_subsystem_stop(subsystem);
1375 return -1;
1376 }
1377
1378 return 1;
1379}
1380
1381static void mac_shadow_subsystem_free(rdpShadowSubsystem* subsystem)
1382{
1383 macShadowSubsystem* mac = (macShadowSubsystem*)subsystem;
1384
1385 if (!mac)
1386 return;
1387
1388 (void)mac_shadow_subsystem_uninit(subsystem);
1389 if (mac->awake != kIOPMNullAssertionID)
1390 (void)IOPMAssertionRelease(mac->awake);
1391 region16_uninit(&mac->pending);
1392 DeleteCriticalSection(&mac->clientLock);
1393 DeleteCriticalSection(&mac->frameLock);
1394 if (mac->frameEvent)
1395 (void)CloseHandle(mac->frameEvent);
1396 if (mac->stopEvent)
1397 (void)CloseHandle(mac->stopEvent);
1398 if (mac->lostEvent)
1399 (void)CloseHandle(mac->lostEvent);
1400 free(mac);
1401}
1402
1403WINPR_ATTR_NODISCARD
1404static rdpShadowSubsystem* mac_shadow_subsystem_new(void)
1405{
1406 macShadowSubsystem* mac = calloc(1, sizeof(macShadowSubsystem));
1407
1408 if (!mac)
1409 return nullptr;
1410
1411 region16_init(&mac->pending);
1412 if (!InitializeCriticalSectionAndSpinCount(&mac->frameLock, 4000))
1413 {
1414 free(mac);
1415 return nullptr;
1416 }
1417
1418 if (!InitializeCriticalSectionAndSpinCount(&mac->clientLock, 4000))
1419 {
1420 DeleteCriticalSection(&mac->frameLock);
1421 free(mac);
1422 return nullptr;
1423 }
1424
1425 mac->frameEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr);
1426 mac->stopEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr);
1427 mac->lostEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr);
1428 if (!mac->frameEvent || !mac->stopEvent || !mac->lostEvent)
1429 {
1430 mac_shadow_subsystem_free(&mac->common);
1431 return nullptr;
1432 }
1433
1434 mac->common.SynchronizeEvent = mac_shadow_input_synchronize_event;
1435 mac->common.KeyboardEvent = mac_shadow_input_keyboard_event;
1436 mac->common.UnicodeKeyboardEvent = mac_shadow_input_unicode_keyboard_event;
1437 mac->common.MouseEvent = mac_shadow_input_mouse_event;
1438 mac->common.ExtendedMouseEvent = mac_shadow_input_extended_mouse_event;
1439 mac->common.ClientConnect = mac_shadow_client_connect;
1440 mac->common.ClientDisconnect = mac_shadow_client_disconnect;
1441#ifdef WITH_PAM
1442 mac->common.Authenticate = mac_shadow_pam_authenticate;
1443#endif
1444 return &mac->common;
1445}
1446
1447FREERDP_API const char* ShadowSubsystemName(void)
1448{
1449 return "Mac";
1450}
1451
1452FREERDP_API int ShadowSubsystemEntry(RDP_SHADOW_ENTRY_POINTS* pEntryPoints)
1453{
1454 pEntryPoints->New = mac_shadow_subsystem_new;
1455 pEntryPoints->Free = mac_shadow_subsystem_free;
1456 pEntryPoints->Init = mac_shadow_subsystem_init;
1457 pEntryPoints->Uninit = mac_shadow_subsystem_uninit;
1458 pEntryPoints->Start = mac_shadow_subsystem_start;
1459 pEntryPoints->Stop = mac_shadow_subsystem_stop;
1460 pEntryPoints->EnumMonitors = mac_shadow_enum_monitors;
1461 return 1;
1462}