@desklink/host 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/NOTICE +3 -0
  2. package/README.md +72 -16
  3. package/bin/desklink-host.mjs +14 -3
  4. package/dist/bridge.d.ts.map +1 -1
  5. package/dist/bridge.js +5 -1
  6. package/dist/bridge.js.map +1 -1
  7. package/dist/engineProcess.d.ts.map +1 -1
  8. package/dist/engineProcess.js +17 -5
  9. package/dist/engineProcess.js.map +1 -1
  10. package/dist/index.d.ts +1 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +1 -0
  13. package/dist/index.js.map +1 -1
  14. package/dist/keeper.d.ts +52 -0
  15. package/dist/keeper.d.ts.map +1 -0
  16. package/dist/keeper.js +101 -0
  17. package/dist/keeper.js.map +1 -0
  18. package/dist/protocol.d.ts +42 -5
  19. package/dist/protocol.d.ts.map +1 -1
  20. package/dist/protocol.js +1 -1
  21. package/dist/protocol.js.map +1 -1
  22. package/dist/resolveEngine.d.ts +3 -2
  23. package/dist/resolveEngine.d.ts.map +1 -1
  24. package/dist/resolveEngine.js +20 -5
  25. package/dist/resolveEngine.js.map +1 -1
  26. package/docs/BOUNDARY.md +11 -6
  27. package/docs/PROTOCOL.md +297 -33
  28. package/engine/Cargo.lock +196 -1
  29. package/engine/Cargo.toml +26 -5
  30. package/engine/build.rs +98 -23
  31. package/engine/native/agent_overlay_mac.m +133 -0
  32. package/engine/native/agent_overlay_wayland.c +192 -0
  33. package/engine/native/agent_overlay_x11.c +103 -0
  34. package/engine/native/mac_stream.mm +188 -0
  35. package/engine/src/capture.rs +169 -19
  36. package/engine/src/capture_mac.rs +145 -0
  37. package/engine/src/clipboard_mac.rs +182 -0
  38. package/engine/src/convert.rs +57 -1
  39. package/engine/src/indicator.rs +162 -0
  40. package/engine/src/input.rs +12 -0
  41. package/engine/src/input_mac.rs +312 -0
  42. package/engine/src/keeper.rs +622 -0
  43. package/engine/src/keymap.rs +25 -0
  44. package/engine/src/keymap_mac.rs +209 -0
  45. package/engine/src/mac.rs +722 -0
  46. package/engine/src/main.rs +158 -2
  47. package/engine/src/peer.rs +429 -22
  48. package/engine/src/portal.rs +4 -0
  49. package/engine/src/portal_mac.rs +68 -0
  50. package/engine/src/protocol.rs +186 -3
  51. package/engine/src/session.rs +1307 -100
  52. package/engine/src/x11.rs +23 -3
  53. package/engine/src/x11_mac.rs +40 -0
  54. package/examples/reference-client.html +33 -4
  55. package/package.json +2 -2
@@ -0,0 +1,192 @@
1
+ // Minimal wlroots layer-shell surface: transparent, unfocusable and input-empty.
2
+ // Only the portal path uses it; X11 uses a cursor sprite instead.
3
+ // Protocol interface names/signatures from wlr-layer-shell-unstable-v1:
4
+ // Copyright (c) 2017 Drew DeVault. Permission is hereby granted to use, copy,
5
+ // modify and distribute this protocol for any purpose without fee, provided
6
+ // this notice appears in all copies. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT
7
+ // WARRANTY OF ANY KIND; THE AUTHORS ARE NOT LIABLE FOR ANY CLAIM OR DAMAGES.
8
+ #define _GNU_SOURCE
9
+ #include <wayland-client.h>
10
+ #include <wayland-client-protocol.h>
11
+ #include <sys/mman.h>
12
+ #include <sys/poll.h>
13
+ #include <time.h>
14
+ #include <unistd.h>
15
+ #include <fcntl.h>
16
+ #include <math.h>
17
+ #include <stdio.h>
18
+ #include <stdlib.h>
19
+ #include <string.h>
20
+
21
+ // Layer-shell v1 protocol (Drew DeVault, 2017; MIT license).
22
+ static const struct wl_interface layer_surface_interface;
23
+ static const struct wl_interface *get_surface_types[] = {
24
+ &layer_surface_interface, &wl_surface_interface, &wl_output_interface, NULL
25
+ };
26
+ static const struct wl_message shell_requests[] = {
27
+ {"get_layer_surface", "no?ous", get_surface_types}, {"destroy", "3", NULL}
28
+ };
29
+ static const struct wl_interface shell_interface = {"zwlr_layer_shell_v1", 1, 2, shell_requests, 0, NULL};
30
+ static const struct wl_message surface_requests[] = {
31
+ {"set_size", "uu", NULL}, {"set_anchor", "u", NULL}, {"set_exclusive_zone", "i", NULL},
32
+ {"set_margin", "iiii", NULL}, {"set_keyboard_interactivity", "u", NULL},
33
+ {"get_popup", "o", NULL}, {"ack_configure", "u", NULL}, {"destroy", "", NULL}
34
+ };
35
+ static const struct wl_message surface_events[] = { {"configure", "uuu", NULL}, {"closed", "", NULL} };
36
+ static const struct wl_interface layer_surface_interface = {"zwlr_layer_surface_v1", 1, 8, surface_requests, 2, surface_events};
37
+
38
+ int desklink_wayland_geometry(int source_w, int source_h, int surface_w, int surface_h,
39
+ double px, double py, double *x, double *y) {
40
+ if (source_w<=0||source_h<=0||surface_w<=0||surface_h<=0) return 0;
41
+ *x=px*surface_w/source_w;*y=py*surface_h/source_h;
42
+ return 1;
43
+ }
44
+ void desklink_wayland_policy(uint32_t *anchor, uint32_t *keyboard_focus, uint32_t *empty_input) {
45
+ *anchor=15; *keyboard_focus=0; *empty_input=1;
46
+ }
47
+
48
+ struct Buffer { struct wl_buffer *handle; uint32_t *pixels; size_t length; int busy; };
49
+ struct Overlay {
50
+ struct wl_display *display;
51
+ struct wl_compositor *compositor;
52
+ struct wl_shm *shm;
53
+ struct wl_proxy *shell, *layer;
54
+ struct wl_surface *surface;
55
+ struct Buffer buffers[2];
56
+ int width, height, source_w, source_h, configured, closed;
57
+ };
58
+ static double now(void) { struct timespec t; clock_gettime(CLOCK_MONOTONIC,&t); return t.tv_sec+t.tv_nsec/1e9; }
59
+ static void registry_global(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) {
60
+ struct Overlay *o=data;
61
+ if (!strcmp(interface,"wl_compositor")) o->compositor=wl_registry_bind(registry,name,&wl_compositor_interface,version<4?version:4);
62
+ if (!strcmp(interface,"wl_shm")) o->shm=wl_registry_bind(registry,name,&wl_shm_interface,1);
63
+ if (!strcmp(interface,"zwlr_layer_shell_v1")) o->shell=wl_registry_bind(registry,name,&shell_interface,1);
64
+ }
65
+ static void registry_remove(void *data, struct wl_registry *registry, uint32_t name) { (void)data;(void)registry;(void)name; }
66
+ static const struct wl_registry_listener registry_listener={registry_global,registry_remove};
67
+ static void configured(void *data, struct wl_proxy *layer, uint32_t serial, uint32_t w, uint32_t h) {
68
+ struct Overlay *o=data; o->width=(int)w; o->height=(int)h; o->configured=1;
69
+ wl_proxy_marshal_flags(layer,6,NULL,1,0,serial);
70
+ }
71
+ static void closed(void *data, struct wl_proxy *layer) { (void)layer; ((struct Overlay *)data)->closed=1; }
72
+ static void (*layer_listener[])(void)={(void (*)(void))configured,(void (*)(void))closed};
73
+ static void buffer_release(void *data, struct wl_buffer *buffer) { (void)buffer; ((struct Buffer *)data)->busy=0; }
74
+ static const struct wl_buffer_listener buffer_listener={buffer_release};
75
+
76
+ static int create_buffer(struct Overlay *o, struct Buffer *b) {
77
+ b->length=(size_t)o->width*o->height*4;
78
+ int fd=memfd_create("desklink-agent-overlay",MFD_CLOEXEC);
79
+ if (fd<0 || ftruncate(fd,b->length)) { if(fd>=0)close(fd); return -1; }
80
+ b->pixels=mmap(NULL,b->length,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
81
+ if (b->pixels==MAP_FAILED) {close(fd);return -1;}
82
+ struct wl_shm_pool *pool=wl_shm_create_pool(o->shm,fd,(int)b->length);
83
+ b->handle=wl_shm_pool_create_buffer(pool,0,o->width,o->height,o->width*4,WL_SHM_FORMAT_ARGB8888);
84
+ wl_shm_pool_destroy(pool); close(fd);
85
+ wl_buffer_add_listener(b->handle,&buffer_listener,b);
86
+ return 0;
87
+ }
88
+ static void blend(uint32_t *pixel, unsigned alpha, unsigned rgb) {
89
+ // premultiplied ARGB8888: the compositor blends this over the desktop.
90
+ *pixel=(alpha<<24)|(((rgb>>16)&255)*alpha/255<<16)|(((rgb>>8)&255)*alpha/255<<8)|((rgb&255)*alpha/255);
91
+ }
92
+ static void draw(struct Overlay *o, struct Buffer *b, double x, double y, double click, double typed, double ending, int positioned) {
93
+ double t=now(), opacity=ending?fmax(0,1-(t-ending)/0.6):1;
94
+ memset(b->pixels,0,b->length);
95
+ int w=o->width,h=o->height;
96
+ for (int edge=0;edge<7;edge++) {
97
+ unsigned alpha=(unsigned)((7-edge)*1.8*opacity);
98
+ for(int px=edge;px<w-edge;px++) {
99
+ blend(&b->pixels[(size_t)edge*w+px],alpha,0x4c9ed0);
100
+ blend(&b->pixels[(size_t)(h-1-edge)*w+px],alpha,0x4c9ed0);
101
+ }
102
+ for(int py=edge;py<h-edge;py++) {
103
+ blend(&b->pixels[(size_t)py*w+edge],alpha,0x4c9ed0);
104
+ blend(&b->pixels[(size_t)py*w+w-1-edge],alpha,0x4c9ed0);
105
+ }
106
+ }
107
+ int left=fmax(0,x-55),right=fmin(w,x+55),top=fmax(0,y-55),bottom=fmin(h,y+55);
108
+ if(positioned)for(int py=top;py<bottom;py++)for(int px=left;px<right;px++) {
109
+ double distance=hypot(px-x,py-y);
110
+ double ring=fmax(0,1-fabs(distance-13)/5)*0.18;
111
+ ring+=fmax(0,1-fabs(distance-23)/7)*0.06;
112
+ if(click && t-click<0.55)ring+=fmax(0,1-fabs(distance-(12+37*(t-click)/0.55))/3)*0.42*(1-(t-click)/0.55);
113
+ if(typed && t-typed<0.4)
114
+ ring+=fmax(0,1-fabs(distance-(8+15*(t-typed)/0.4))/3)*0.22*(1-(t-typed)/0.4);
115
+ unsigned alpha=(unsigned)(fmin(1,ring*opacity)*255);
116
+ if(alpha)blend(&b->pixels[(size_t)py*w+px],alpha,0x4c9ed0);
117
+ }
118
+ }
119
+
120
+ int desklink_agent_overlay_wayland(int source_w, int source_h) {
121
+ struct Overlay o={.source_w=source_w,.source_h=source_h};
122
+ o.display=wl_display_connect(NULL);
123
+ if(!o.display)return 2;
124
+ struct wl_registry *registry=wl_display_get_registry(o.display);
125
+ wl_registry_add_listener(registry,&registry_listener,&o);
126
+ wl_display_roundtrip(o.display);
127
+ if(!o.compositor||!o.shm||!o.shell){wl_display_disconnect(o.display);return 2;}
128
+ o.surface=wl_compositor_create_surface(o.compositor);
129
+ o.layer=wl_proxy_marshal_flags(o.shell,0,&layer_surface_interface,1,0,NULL,o.surface,NULL,3,"desklink-agent");
130
+ wl_proxy_add_listener(o.layer,layer_listener,&o);
131
+ uint32_t anchor,keyboard_focus,empty_input;
132
+ desklink_wayland_policy(&anchor,&keyboard_focus,&empty_input);
133
+ wl_proxy_marshal_flags(o.layer,0,NULL,1,0,0u,0u);
134
+ wl_proxy_marshal_flags(o.layer,1,NULL,1,0,anchor);
135
+ wl_proxy_marshal_flags(o.layer,2,NULL,1,0,-1);
136
+ wl_proxy_marshal_flags(o.layer,4,NULL,1,0,keyboard_focus);
137
+ if(empty_input) {
138
+ struct wl_region *empty=wl_compositor_create_region(o.compositor);
139
+ wl_surface_set_input_region(o.surface,empty);
140
+ wl_region_destroy(empty);
141
+ }
142
+ wl_surface_commit(o.surface);
143
+ while(!o.configured&&!o.closed&&wl_display_dispatch(o.display)>=0) {}
144
+ if(o.closed||o.width<=0||o.height<=0){wl_display_disconnect(o.display);return 2;}
145
+ if(create_buffer(&o,&o.buffers[0])||create_buffer(&o,&o.buffers[1])){wl_display_disconnect(o.display);return 2;}
146
+ int flags=fcntl(STDIN_FILENO,F_GETFL);fcntl(STDIN_FILENO,F_SETFL,flags|O_NONBLOCK);
147
+ puts("READY");fflush(stdout);
148
+ double tx=0,ty=0,x=0,y=0,click=0,typed=0,ending=0;
149
+ int active=0,positioned=0;
150
+ char pending[128];size_t used=0;
151
+ while(!o.closed && (!ending||now()-ending<0.6)) {
152
+ struct pollfd fds[2]={{wl_display_get_fd(o.display),POLLIN,0},{STDIN_FILENO,POLLIN|POLLHUP,0}};
153
+ wl_display_flush(o.display);
154
+ poll(fds,2,16);
155
+ if(fds[0].revents&POLLIN && wl_display_dispatch(o.display)<0)break;
156
+ else wl_display_dispatch_pending(o.display);
157
+ char bytes[128];ssize_t n=read(STDIN_FILENO,bytes,sizeof(bytes));
158
+ if(n==0&&!ending)ending=now();
159
+ for(ssize_t i=0;i<n;i++) {
160
+ if(bytes[i]=='\n') {
161
+ pending[used]=0;char kind=0;double px=0,py=0;
162
+ if(sscanf(pending,"%c %lf %lf",&kind,&px,&py)>=1) {
163
+ if(kind=='S')ending=now();
164
+ else if(kind=='M'||kind=='C'||kind=='T'||kind=='A') {
165
+ if(px>=0&&py>=0) {
166
+ if(desklink_wayland_geometry(o.source_w,o.source_h,o.width,o.height,px,py,&tx,&ty)
167
+ && !positioned){x=tx;y=ty;positioned=1;}
168
+ }
169
+ active=1;
170
+ if(kind=='C')click=now();if(kind=='T')typed=now();
171
+ }
172
+ }
173
+ used=0;
174
+ }else if(used<sizeof(pending)-1)pending[used++]=bytes[i];
175
+ }
176
+ if(!active)continue;
177
+ x+=(tx-x)*0.24;y+=(ty-y)*0.24;
178
+ for(int i=0;i<2;i++)if(!o.buffers[i].busy) {
179
+ draw(&o,&o.buffers[i],x,y,click,typed,ending,positioned);
180
+ o.buffers[i].busy=1;
181
+ wl_surface_attach(o.surface,o.buffers[i].handle,0,0);
182
+ wl_surface_damage_buffer(o.surface,0,0,o.width,o.height);
183
+ wl_surface_commit(o.surface);
184
+ break;
185
+ }
186
+ }
187
+ wl_surface_attach(o.surface,NULL,0,0);wl_surface_commit(o.surface);wl_display_roundtrip(o.display);
188
+ for(int i=0;i<2;i++){wl_buffer_destroy(o.buffers[i].handle);munmap(o.buffers[i].pixels,o.buffers[i].length);}
189
+ wl_proxy_destroy(o.layer);wl_surface_destroy(o.surface);wl_proxy_destroy(o.shell);
190
+ wl_display_disconnect(o.display);
191
+ return 0;
192
+ }
@@ -0,0 +1,103 @@
1
+ // The X server draws cursors outside GetImage(root). No overlay window can
2
+ // expose black pixels, intercept input, or poison agent frame diffs.
3
+ #include <X11/Xlib.h>
4
+ #include <X11/Xcursor/Xcursor.h>
5
+ #include <math.h>
6
+ #include <fcntl.h>
7
+ #include <stdio.h>
8
+ #include <string.h>
9
+ #include <time.h>
10
+ #include <unistd.h>
11
+
12
+ static double now(void) { struct timespec t; clock_gettime(CLOCK_MONOTONIC, &t); return t.tv_sec + t.tv_nsec/1e9; }
13
+
14
+ static unsigned pixel(double distance, double radius, double opacity) {
15
+ double edge = fmax(0, 1 - fabs(distance-radius)/2.5);
16
+ unsigned alpha = (unsigned)(fmin(1, edge * opacity) * 255);
17
+ return (alpha << 24) | 0x4c9ed0;
18
+ }
19
+
20
+ int desklink_agent_overlay_main(const char *display_name) {
21
+ Display *d = XOpenDisplay(display_name);
22
+ if (!d) return 2;
23
+ Window root = DefaultRootWindow(d);
24
+ unsigned best_w=64,best_h=64;
25
+ XQueryBestCursor(d,root,64,64,&best_w,&best_h);
26
+ unsigned size = best_w < best_h ? best_w : best_h;
27
+ if (size < 32) { XCloseDisplay(d); return 2; }
28
+ if (size > 96) size = 96;
29
+ XcursorImage *image = XcursorImageCreate(size,size);
30
+ if (!image) { XCloseDisplay(d); return 2; }
31
+ image->version = XCURSOR_IMAGE_VERSION;
32
+ Cursor previous = None;
33
+ int stdin_flags = fcntl(STDIN_FILENO, F_GETFL);
34
+ fcntl(STDIN_FILENO, F_SETFL, stdin_flags | O_NONBLOCK);
35
+ puts("READY"); fflush(stdout);
36
+ double target_x=0,target_y=0,cursor_x=0,cursor_y=0,click=0,typed=0,ending=0;
37
+ int active=0;
38
+ char pending[128]; size_t used=0;
39
+ for (;;) {
40
+ char bytes[128]; ssize_t n = read(STDIN_FILENO, bytes, sizeof(bytes));
41
+ if (n == 0 && !ending) ending=now();
42
+ for (ssize_t i=0; i<n; i++) {
43
+ if (bytes[i]=='\n') {
44
+ pending[used]=0; char kind=0; double px=0,py=0;
45
+ if (sscanf(pending,"%c %lf %lf",&kind,&px,&py)>=1) {
46
+ if (kind=='S') ending=now();
47
+ else if (kind=='M'||kind=='C'||kind=='T') {
48
+ if (px>=0 && py>=0) {
49
+ target_x=px; target_y=py;
50
+ if (!active) { cursor_x=px; cursor_y=py; }
51
+ }
52
+ active=1;
53
+ if (kind=='C') click=now();
54
+ if (kind=='T') typed=now();
55
+ }
56
+ }
57
+ used=0;
58
+ } else if (used < sizeof(pending)-1) pending[used++]=bytes[i];
59
+ }
60
+ double t=now();
61
+ if (ending && t-ending>0.55) break;
62
+ if (active) {
63
+ cursor_x += (target_x-cursor_x)*0.24;
64
+ cursor_y += (target_y-cursor_y)*0.24;
65
+ int radius=(int)size/2-3;
66
+ image->xhot = (unsigned)fmax(0,fmin(size-1,(double)size/2+target_x-cursor_x));
67
+ image->yhot = (unsigned)fmax(0,fmin(size-1,(double)size/2+target_y-cursor_y));
68
+ for (unsigned y=0;y<size;y++) for (unsigned x=0;x<size;x++) {
69
+ double distance=hypot(x-(double)size/2,y-(double)size/2);
70
+ double opacity=ending ? fmax(0,1-(t-ending)/0.55) : 1;
71
+ unsigned color=pixel(distance,12,0.72*opacity);
72
+ if (distance>=16 && distance<=radius) {
73
+ unsigned outer=pixel(distance,20,0.20*opacity);
74
+ if ((outer>>24)>(color>>24)) color=outer;
75
+ }
76
+ if (click && t-click<0.5) {
77
+ double progress=(t-click)/0.5;
78
+ unsigned ripple=pixel(distance,12+(radius-14)*progress,0.6*(1-progress)*opacity);
79
+ if ((ripple>>24)>(color>>24)) color=ripple;
80
+ }
81
+ if (typed && t-typed<0.35) {
82
+ double progress=(t-typed)/0.35;
83
+ if (distance<5+progress*12) {
84
+ unsigned pulse=(unsigned)(0.22*(1-progress)*opacity*255);
85
+ if (pulse>(color>>24)) color=(pulse<<24)|0x5baed8;
86
+ }
87
+ }
88
+ image->pixels[y*size+x]=color;
89
+ }
90
+ Cursor next=XcursorImageLoadCursor(d,image);
91
+ if (next!=None) {
92
+ XDefineCursor(d,root,next); XFlush(d);
93
+ if (previous!=None) XFreeCursor(d,previous);
94
+ previous=next;
95
+ }
96
+ }
97
+ usleep(16000);
98
+ }
99
+ XUndefineCursor(d,root); XFlush(d);
100
+ if (previous!=None) XFreeCursor(d,previous);
101
+ XcursorImageDestroy(image); XCloseDisplay(d);
102
+ return 0;
103
+ }
@@ -0,0 +1,188 @@
1
+ #import <Foundation/Foundation.h>
2
+ #import <ScreenCaptureKit/ScreenCaptureKit.h>
3
+ #import <CoreVideo/CoreVideo.h>
4
+ #import <CoreMedia/CoreMedia.h>
5
+
6
+ using FrameCallback = void (*)(void *, const uint8_t *, size_t, size_t, size_t, size_t);
7
+ using StatusCallback = void (*)(void *, const char *, bool);
8
+
9
+ @interface DLStreamOutput : NSObject <SCStreamOutput>
10
+ @property(nonatomic, assign) void *context;
11
+ @property(nonatomic, assign) FrameCallback callback;
12
+ @end
13
+
14
+ @implementation DLStreamOutput
15
+ - (void)stream:(SCStream *)stream didOutputSampleBuffer:(CMSampleBufferRef)sample ofType:(SCStreamOutputType)type {
16
+ (void)stream;
17
+ if (type != SCStreamOutputTypeScreen || !CMSampleBufferIsValid(sample)) return;
18
+ @autoreleasepool {
19
+ CVPixelBufferRef pixel = CMSampleBufferGetImageBuffer(sample);
20
+ if (!pixel || CVPixelBufferLockBaseAddress(pixel, kCVPixelBufferLock_ReadOnly) != kCVReturnSuccess) return;
21
+ const uint8_t *base = (const uint8_t *)CVPixelBufferGetBaseAddress(pixel);
22
+ if (base && CVPixelBufferGetPixelFormatType(pixel) == kCVPixelFormatType_32BGRA)
23
+ self.callback(self.context, base, CVPixelBufferGetDataSize(pixel),
24
+ CVPixelBufferGetWidth(pixel), CVPixelBufferGetHeight(pixel), CVPixelBufferGetBytesPerRow(pixel));
25
+ CVPixelBufferUnlockBaseAddress(pixel, kCVPixelBufferLock_ReadOnly);
26
+ }
27
+ }
28
+ @end
29
+
30
+ @interface DLStreamSession : NSObject <SCStreamDelegate>
31
+ @property(nonatomic, strong) SCStream *stream;
32
+ @property(nonatomic, strong) SCContentFilter *filter;
33
+ @property(nonatomic, strong) SCStreamConfiguration *config;
34
+ @property(nonatomic, strong) DLStreamOutput *output;
35
+ @property(nonatomic, strong) dispatch_queue_t queue;
36
+ @property(nonatomic, assign) StatusCallback status;
37
+ @property(nonatomic, assign) void *context;
38
+ @property(atomic, assign) BOOL closing;
39
+ @property(atomic, assign) BOOL restarting;
40
+ - (NSError *)startStream;
41
+ @end
42
+ @implementation DLStreamSession
43
+ - (NSError *)startStream {
44
+ __block NSError *failure = nil;
45
+ self.stream = [[SCStream alloc] initWithFilter:self.filter configuration:self.config delegate:self];
46
+ if (![self.stream addStreamOutput:self.output type:SCStreamOutputTypeScreen
47
+ sampleHandlerQueue:self.queue error:&failure]) return failure;
48
+ dispatch_semaphore_t started = dispatch_semaphore_create(0);
49
+ [self.stream startCaptureWithCompletionHandler:^(NSError *err) {
50
+ failure = err;
51
+ dispatch_semaphore_signal(started);
52
+ }];
53
+ if (dispatch_semaphore_wait(started, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)))
54
+ return [NSError errorWithDomain:@"desklink.capture" code:1 userInfo:@{NSLocalizedDescriptionKey: @"Timed out starting stream"}];
55
+ return failure;
56
+ }
57
+ - (void)stream:(SCStream *)stream didStopWithError:(NSError *)error {
58
+ if (self.closing || self.restarting || stream != self.stream) return;
59
+ self.restarting = YES;
60
+ NSString *reason = [NSString stringWithFormat:@"SCStream stopped (%@ %ld): %@", error.domain, (long)error.code, error.localizedDescription];
61
+ self.status(self.context, reason.UTF8String, false);
62
+ NSLog(@"%@; restarting capture", reason);
63
+ dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
64
+ for (int attempt = 1; attempt <= 3 && !self.closing; attempt++) {
65
+ [NSThread sleepForTimeInterval:attempt];
66
+ if (self.closing) break;
67
+ NSError *failure = [self startStream];
68
+ if (!failure) {
69
+ if (!self.closing) self.status(self.context, "SCStream restarted", true);
70
+ self.restarting = NO;
71
+ return;
72
+ }
73
+ NSLog(@"SCStream restart %d failed: %@", attempt, failure);
74
+ }
75
+ if (!self.closing) self.status(self.context, "SCStream restart exhausted", false);
76
+ self.restarting = NO;
77
+ });
78
+ }
79
+ @end
80
+
81
+ static void fail(char *error, size_t capacity, NSString *message) {
82
+ if (capacity) snprintf(error, capacity, "%s", message.UTF8String ?: "ScreenCaptureKit failed");
83
+ }
84
+
85
+ extern "C" void *dl_mac_stream_start(uint32_t display_id, size_t width, size_t height, uint32_t fps,
86
+ uint32_t indicator_pid, void *context, FrameCallback callback,
87
+ StatusCallback status, char *error, size_t capacity) {
88
+ @autoreleasepool {
89
+ dispatch_semaphore_t found = dispatch_semaphore_create(0);
90
+ __block SCShareableContent *content = nil;
91
+ __block NSError *failure = nil;
92
+ [SCShareableContent getShareableContentWithCompletionHandler:^(SCShareableContent *value, NSError *err) {
93
+ content = value;
94
+ failure = err;
95
+ dispatch_semaphore_signal(found);
96
+ }];
97
+ if (dispatch_semaphore_wait(found, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC))) {
98
+ fail(error, capacity, @"Timed out listing displays");
99
+ return nullptr;
100
+ }
101
+ if (failure) { fail(error, capacity, failure.localizedDescription); return nullptr; }
102
+ SCDisplay *display = nil;
103
+ for (SCDisplay *candidate in content.displays)
104
+ if (candidate.displayID == display_id) { display = candidate; break; }
105
+ if (!display) { fail(error, capacity, @"Selected display is unavailable"); return nullptr; }
106
+ NSMutableArray<SCWindow *> *excluded = [NSMutableArray array];
107
+ if (indicator_pid) {
108
+ for (SCWindow *window in content.windows)
109
+ if (window.owningApplication.processID == (pid_t)indicator_pid)
110
+ [excluded addObject:window];
111
+ if (!excluded.count) {
112
+ fail(error, capacity, @"Agent indicator window not found for capture exclusion");
113
+ return nullptr;
114
+ }
115
+ }
116
+ SCContentFilter *filter = [[SCContentFilter alloc] initWithDisplay:display excludingWindows:excluded];
117
+ SCStreamConfiguration *config = [SCStreamConfiguration new];
118
+ config.width = width;
119
+ config.height = height;
120
+ config.pixelFormat = kCVPixelFormatType_32BGRA;
121
+ config.minimumFrameInterval = CMTimeMake(1, fps ? fps : 30);
122
+ config.queueDepth = 3;
123
+ config.showsCursor = YES;
124
+ config.capturesAudio = NO;
125
+ DLStreamSession *session = [DLStreamSession new];
126
+ session.output = [DLStreamOutput new];
127
+ session.output.context = context;
128
+ session.output.callback = callback;
129
+ session.queue = dispatch_queue_create("dev.desklink.capture", DISPATCH_QUEUE_SERIAL);
130
+ session.filter = filter;
131
+ session.config = config;
132
+ session.status = status;
133
+ session.context = context;
134
+ failure = [session startStream];
135
+ if (failure) {
136
+ fail(error, capacity, failure.localizedDescription);
137
+ dispatch_semaphore_t stopped = dispatch_semaphore_create(0);
138
+ [session.stream stopCaptureWithCompletionHandler:^(NSError *err) {
139
+ (void)err;
140
+ dispatch_semaphore_signal(stopped);
141
+ }];
142
+ dispatch_semaphore_wait(stopped, DISPATCH_TIME_FOREVER);
143
+ dispatch_sync(session.queue, ^{});
144
+ return nullptr;
145
+ }
146
+ const char *simulate = getenv("DESKLINK_AXI_SIMULATE_STREAM_STOP_MS");
147
+ int delay = simulate ? atoi(simulate) : 0;
148
+ if (delay >= 100 && delay <= 60000) {
149
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)delay * NSEC_PER_MSEC),
150
+ dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
151
+ if (session.closing) return;
152
+ SCStream *stream = session.stream;
153
+ [stream stopCaptureWithCompletionHandler:^(NSError *err) {
154
+ (void)err;
155
+ NSError *stopped = [NSError errorWithDomain:@"SCStreamErrorDomain" code:-3821
156
+ userInfo:@{NSLocalizedDescriptionKey: @"simulated systemStoppedStream"}];
157
+ [session stream:stream didStopWithError:stopped];
158
+ }];
159
+ });
160
+ }
161
+ return (__bridge_retained void *)session;
162
+ }
163
+ }
164
+
165
+ extern "C" bool dl_mac_stream_stop(void *handle) {
166
+ if (!handle) return true;
167
+ @autoreleasepool {
168
+ DLStreamSession *session = (__bridge_transfer DLStreamSession *)handle;
169
+ session.closing = YES;
170
+ for (int i = 0; session.restarting && i < 350; i++) [NSThread sleepForTimeInterval:0.1];
171
+ if (session.restarting) {
172
+ (void)CFBridgingRetain(session);
173
+ return false; // The restart still owns the callback context.
174
+ }
175
+ dispatch_semaphore_t stopped = dispatch_semaphore_create(0);
176
+ [session.stream stopCaptureWithCompletionHandler:^(NSError *err) {
177
+ (void)err;
178
+ dispatch_semaphore_signal(stopped);
179
+ }];
180
+ if (dispatch_semaphore_wait(stopped, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC))) {
181
+ // A late frame can still use the callback context; keep both alive rather than free it.
182
+ (void)CFBridgingRetain(session);
183
+ return false;
184
+ }
185
+ dispatch_sync(session.queue, ^{});
186
+ return true;
187
+ }
188
+ }