@desklink/host 0.3.0 → 0.4.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.
- package/NOTICE +3 -0
- package/README.md +92 -23
- package/bin/desklink-host.mjs +14 -5
- package/dist/bridge.d.ts +3 -4
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js +18 -7
- package/dist/bridge.js.map +1 -1
- package/dist/engineProcess.d.ts +6 -1
- package/dist/engineProcess.d.ts.map +1 -1
- package/dist/engineProcess.js +34 -5
- package/dist/engineProcess.js.map +1 -1
- package/dist/protocol.d.ts +35 -2
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js.map +1 -1
- package/dist/resolveEngine.d.ts +6 -0
- package/dist/resolveEngine.d.ts.map +1 -1
- package/dist/resolveEngine.js +45 -6
- package/dist/resolveEngine.js.map +1 -1
- package/docs/BOUNDARY.md +8 -8
- package/docs/PROTOCOL.md +185 -45
- package/engine/Cargo.lock +2 -2
- package/engine/Cargo.toml +7 -12
- package/engine/build.rs +62 -5
- package/engine/native/agent_indicator.h +57 -0
- package/engine/native/agent_overlay_mac.m +51 -4
- package/engine/native/agent_overlay_wayland.c +189 -35
- package/engine/native/agent_overlay_x11.c +186 -55
- package/engine/src/capture.rs +96 -5
- package/engine/src/capture_win.rs +23 -0
- package/engine/src/clipboard.rs +174 -0
- package/engine/src/clipboard_win.rs +8 -0
- package/engine/src/cursor.rs +118 -0
- package/engine/src/indicator.rs +271 -5
- package/engine/src/input_win.rs +91 -0
- package/engine/src/keymap_win.rs +39 -0
- package/engine/src/mac.rs +28 -6
- package/engine/src/main.rs +167 -25
- package/engine/src/peer.rs +10 -4
- package/engine/src/portal.rs +42 -4
- package/engine/src/portal_win.rs +37 -0
- package/engine/src/protocol.rs +56 -10
- package/engine/src/session.rs +749 -203
- package/engine/src/win.rs +110 -0
- package/engine/src/x11.rs +44 -29
- package/engine/src/x11_mac.rs +4 -0
- package/examples/reference-client.html +3 -5
- package/package.json +4 -4
|
@@ -1,40 +1,82 @@
|
|
|
1
|
-
// The
|
|
2
|
-
//
|
|
1
|
+
// The agent cue is a click-through override-redirect window that follows the
|
|
2
|
+
// pointer, so it stays visible over windows that set their own cursor and never
|
|
3
|
+
// replaces the person's cursor. With a compositing manager it uses an ARGB
|
|
4
|
+
// visual; without one, a 1-bit shape. It appears in GetImage(root), so each
|
|
5
|
+
// state is reported on stdout before it is drawn and the engine restores the
|
|
6
|
+
// covered pixels in X11 capture (desklink_indicator_covered below).
|
|
3
7
|
#include <X11/Xlib.h>
|
|
4
|
-
#include <X11/
|
|
8
|
+
#include <X11/Xutil.h>
|
|
9
|
+
#include <X11/Xresource.h>
|
|
10
|
+
#include <X11/extensions/shape.h>
|
|
5
11
|
#include <math.h>
|
|
6
12
|
#include <fcntl.h>
|
|
7
13
|
#include <stdio.h>
|
|
14
|
+
#include <stdlib.h>
|
|
8
15
|
#include <string.h>
|
|
9
16
|
#include <time.h>
|
|
10
17
|
#include <unistd.h>
|
|
18
|
+
#include "agent_indicator.h"
|
|
19
|
+
|
|
20
|
+
// The same coverage test the helper draws with, for the engine's capture mask.
|
|
21
|
+
int desklink_indicator_covered(double x, double y, double opacity, double click, double typed, unsigned threshold, double reveal) {
|
|
22
|
+
return hypot(x,y)<reveal && (indicator_pixel(x,y,opacity,click,typed)>>24) >= threshold;
|
|
23
|
+
}
|
|
11
24
|
|
|
12
25
|
static double now(void) { struct timespec t; clock_gettime(CLOCK_MONOTONIC, &t); return t.tv_sec + t.tv_nsec/1e9; }
|
|
13
26
|
|
|
14
|
-
static
|
|
15
|
-
double
|
|
16
|
-
|
|
17
|
-
|
|
27
|
+
static double desktop_scale(Display *d) {
|
|
28
|
+
double scale=1;
|
|
29
|
+
const char *resources=XResourceManagerString(d);
|
|
30
|
+
if(!resources) return scale;
|
|
31
|
+
XrmInitialize(); XrmDatabase db=XrmGetStringDatabase(resources);
|
|
32
|
+
XrmValue value; char *type;
|
|
33
|
+
if(db && XrmGetResource(db,"Xft.dpi","Xft.Dpi",&type,&value) && value.addr) {
|
|
34
|
+
double dpi=strtod(value.addr,NULL);
|
|
35
|
+
if(isfinite(dpi) && dpi>=96 && dpi<=384)scale=dpi/96;
|
|
36
|
+
}
|
|
37
|
+
if(db)XrmDestroyDatabase(db);
|
|
38
|
+
return scale;
|
|
18
39
|
}
|
|
19
40
|
|
|
20
41
|
int desklink_agent_overlay_main(const char *display_name) {
|
|
21
42
|
Display *d = XOpenDisplay(display_name);
|
|
22
43
|
if (!d) return 2;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
44
|
+
int event_base, error_base, screen=DefaultScreen(d);
|
|
45
|
+
if (!XShapeQueryExtension(d,&event_base,&error_base)) { XCloseDisplay(d); return 2; }
|
|
46
|
+
Window root = RootWindow(d,screen);
|
|
47
|
+
double scale=desktop_scale(d);
|
|
48
|
+
// The arrow reaches 36 logical px from the cue's center.
|
|
49
|
+
int size=(int)ceil(80*scale);
|
|
50
|
+
char selection[32]; snprintf(selection,sizeof(selection),"_NET_WM_CM_S%d",screen);
|
|
51
|
+
XVisualInfo argb;
|
|
52
|
+
int alpha = XGetSelectionOwner(d,XInternAtom(d,selection,False))!=None
|
|
53
|
+
&& XMatchVisualInfo(d,screen,32,TrueColor,&argb);
|
|
54
|
+
XSetWindowAttributes attrs={0};
|
|
55
|
+
attrs.override_redirect=True;
|
|
56
|
+
attrs.background_pixmap=None;
|
|
57
|
+
attrs.border_pixel=0;
|
|
58
|
+
unsigned long mask=CWOverrideRedirect|CWBackPixmap|CWBorderPixel;
|
|
59
|
+
Visual *visual=DefaultVisual(d,screen); int depth=DefaultDepth(d,screen);
|
|
60
|
+
if(alpha) {
|
|
61
|
+
visual=argb.visual; depth=32;
|
|
62
|
+
attrs.colormap=XCreateColormap(d,root,visual,AllocNone); mask|=CWColormap;
|
|
63
|
+
} else if(depth<24) { XCloseDisplay(d); return 2; }
|
|
64
|
+
Window window=XCreateWindow(d,root,0,0,size,size,0,depth,InputOutput,visual,mask,&attrs);
|
|
65
|
+
XStoreName(d,window,"Desklink agent");
|
|
66
|
+
XShapeCombineRectangles(d,window,ShapeInput,0,0,NULL,0,ShapeSet,Unsorted);
|
|
67
|
+
XSelectInput(d,window,ExposureMask);
|
|
68
|
+
XSelectInput(d,root,SubstructureNotifyMask);
|
|
69
|
+
GC gc=XCreateGC(d,window,0,NULL);
|
|
70
|
+
uint32_t *pixels=calloc((size_t)size*size,4);
|
|
71
|
+
int row=(size+7)/8;
|
|
72
|
+
char *bits=calloc((size_t)row*size,1);
|
|
73
|
+
XImage *image=pixels?XCreateImage(d,visual,depth,ZPixmap,0,(char *)pixels,size,size,32,0):NULL;
|
|
74
|
+
if(!image || !bits || image->bits_per_pixel!=32) { XCloseDisplay(d); return 2; }
|
|
33
75
|
int stdin_flags = fcntl(STDIN_FILENO, F_GETFL);
|
|
34
76
|
fcntl(STDIN_FILENO, F_SETFL, stdin_flags | O_NONBLOCK);
|
|
35
77
|
puts("READY"); fflush(stdout);
|
|
36
|
-
double target_x=0,target_y=0,
|
|
37
|
-
int active=0;
|
|
78
|
+
double target_x=0,target_y=0,cue_x=0,cue_y=0,click=0,typed=0,ending=0,started=0;
|
|
79
|
+
int active=0,mapped=0,redraw=0,was_animating=0,last_x=-100000,last_y=-100000;
|
|
38
80
|
char pending[128]; size_t used=0;
|
|
39
81
|
for (;;) {
|
|
40
82
|
char bytes[128]; ssize_t n = read(STDIN_FILENO, bytes, sizeof(bytes));
|
|
@@ -44,11 +86,12 @@ int desklink_agent_overlay_main(const char *display_name) {
|
|
|
44
86
|
pending[used]=0; char kind=0; double px=0,py=0;
|
|
45
87
|
if (sscanf(pending,"%c %lf %lf",&kind,&px,&py)>=1) {
|
|
46
88
|
if (kind=='S') ending=now();
|
|
47
|
-
else if (kind=='M'||kind=='C'||kind=='T') {
|
|
89
|
+
else if ((kind=='M'||kind=='C'||kind=='T') && (active || (px>=0 && py>=0))) {
|
|
48
90
|
if (px>=0 && py>=0) {
|
|
49
91
|
target_x=px; target_y=py;
|
|
50
|
-
if (!active) {
|
|
92
|
+
if (!active) { cue_x=px; cue_y=py; }
|
|
51
93
|
}
|
|
94
|
+
if (!active) started=now();
|
|
52
95
|
active=1;
|
|
53
96
|
if (kind=='C') click=now();
|
|
54
97
|
if (kind=='T') typed=now();
|
|
@@ -57,47 +100,135 @@ int desklink_agent_overlay_main(const char *display_name) {
|
|
|
57
100
|
used=0;
|
|
58
101
|
} else if (used < sizeof(pending)-1) pending[used++]=bytes[i];
|
|
59
102
|
}
|
|
103
|
+
while (XPending(d)) {
|
|
104
|
+
XEvent event; XNextEvent(d,&event);
|
|
105
|
+
if (event.type==Expose && event.xexpose.window==window) redraw=1;
|
|
106
|
+
// Stay above windows and menus mapped or restacked after us.
|
|
107
|
+
if (mapped && ((event.type==MapNotify && event.xmap.window!=window)
|
|
108
|
+
|| (event.type==ConfigureNotify && event.xconfigure.window!=window))) XRaiseWindow(d,window);
|
|
109
|
+
}
|
|
60
110
|
double t=now();
|
|
61
|
-
if (ending && t-ending>0.55) break;
|
|
111
|
+
if (ending && (!active || t-ending>0.55)) break;
|
|
62
112
|
if (active) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
113
|
+
cue_x += (target_x-cue_x)*0.24;
|
|
114
|
+
cue_y += (target_y-cue_y)*0.24;
|
|
115
|
+
double opacity=ending ? fmax(0,1-(t-ending)/0.55) : fmin(1,(t-started)/0.18);
|
|
116
|
+
double c=click?(t-click)/0.55:-1, k=typed?(t-typed)/0.4:-1;
|
|
117
|
+
int animating = opacity<1 || (c>=0 && c<1) || (k>=0 && k<1);
|
|
118
|
+
// One more frame once an animation ends, so the rest state is exact.
|
|
119
|
+
if (!animating && was_animating) redraw=1;
|
|
120
|
+
was_animating=animating;
|
|
121
|
+
int x=(int)lround(cue_x), y=(int)lround(cue_y);
|
|
122
|
+
if (animating || redraw || x!=last_x || y!=last_y) {
|
|
123
|
+
// A 1-bit shape cannot fade: reveal outward from the tip and
|
|
124
|
+
// retract inward, so coverage only grows while appearing and a
|
|
125
|
+
// still cue never uncovers stale pixels.
|
|
126
|
+
double unit=scale, reveal=alpha?99:40*opacity*(2-opacity);
|
|
127
|
+
memset(bits,0,(size_t)row*size);
|
|
128
|
+
for (int py=0;py<size;py++) for (int px=0;px<size;px++) {
|
|
129
|
+
double lx=(px-size/2.0)/unit, ly=(py-size/2.0)/unit;
|
|
130
|
+
uint32_t color=hypot(lx,ly)<reveal?indicator_pixel(lx,ly,alpha?opacity:1,c,k):0;
|
|
131
|
+
unsigned a=color>>24;
|
|
132
|
+
if (alpha) { pixels[py*size+px]=color; continue; }
|
|
133
|
+
if (a<128) { pixels[py*size+px]=0; continue; }
|
|
134
|
+
bits[py*row+px/8]|=(char)(1<<(px%8));
|
|
135
|
+
// Un-premultiply: the shaped path paints opaque pixels.
|
|
136
|
+
pixels[py*size+px]=((((color>>16)&255)*255/a)<<16)|((((color>>8)&255)*255/a)<<8)|((color&255)*255/a);
|
|
75
137
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
138
|
+
// Announce first: capture reads this pipe after each grab, so
|
|
139
|
+
// every state a grab can see is already announced.
|
|
140
|
+
printf("P %d %d %d %.6f %.4f %.4f %.4f %u %.4f\n",x-size/2,y-size/2,size,unit,alpha?opacity:1,c,k,alpha?1u:128u,reveal);
|
|
141
|
+
// No reader means the engine is gone: leave at once rather
|
|
142
|
+
// than animate unmasked into whatever captures next.
|
|
143
|
+
if (fflush(stdout)) break;
|
|
144
|
+
if (!alpha) {
|
|
145
|
+
Pixmap shape=XCreateBitmapFromData(d,window,bits,size,size);
|
|
146
|
+
XShapeCombineMask(d,window,ShapeBounding,0,0,shape,ShapeSet);
|
|
147
|
+
XFreePixmap(d,shape);
|
|
80
148
|
}
|
|
81
|
-
if (
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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;
|
|
149
|
+
if (x!=last_x || y!=last_y) XMoveWindow(d,window,x-size/2,y-size/2);
|
|
150
|
+
if (!mapped) { XMapRaised(d,window); mapped=1; }
|
|
151
|
+
XPutImage(d,window,gc,image,0,0,0,0,size,size);
|
|
152
|
+
// Wait until the server has drawn it, so at most the newest
|
|
153
|
+
// announced state is not on screen yet.
|
|
154
|
+
XSync(d,False);
|
|
155
|
+
last_x=x; last_y=y; redraw=0;
|
|
95
156
|
}
|
|
96
157
|
}
|
|
97
158
|
usleep(16000);
|
|
98
159
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
160
|
+
XDestroyWindow(d,window);
|
|
161
|
+
XDestroyImage(image); free(bits);
|
|
162
|
+
XFreeGC(d,gc); XCloseDisplay(d);
|
|
163
|
+
return 0;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Point cues are independent of the real cursor. A shaped override-redirect
|
|
167
|
+
// window works without a compositor; an empty input shape passes every event
|
|
168
|
+
// through and mapping it never asks for keyboard focus. It intentionally appears
|
|
169
|
+
// in GetImage(root), so the local person and the video observer share the cue.
|
|
170
|
+
|
|
171
|
+
int desklink_point_overlay_main(const char *display_name) {
|
|
172
|
+
Display *d = XOpenDisplay(display_name);
|
|
173
|
+
if (!d) return 2;
|
|
174
|
+
int event_base, error_base;
|
|
175
|
+
if (!XShapeQueryExtension(d, &event_base, &error_base)) { XCloseDisplay(d); return 2; }
|
|
176
|
+
int screen = DefaultScreen(d), width = DisplayWidth(d, screen), height = DisplayHeight(d, screen);
|
|
177
|
+
Window root = RootWindow(d, screen);
|
|
178
|
+
XSetWindowAttributes attrs = {0};
|
|
179
|
+
attrs.override_redirect = True;
|
|
180
|
+
attrs.background_pixel = 0x4c9ed0;
|
|
181
|
+
Window window = XCreateWindow(d, root, 0, 0, width, height, 0,
|
|
182
|
+
CopyFromParent, InputOutput, CopyFromParent, CWOverrideRedirect | CWBackPixel, &attrs);
|
|
183
|
+
XStoreName(d, window, "Desklink point");
|
|
184
|
+
XShapeCombineRectangles(d, window, ShapeInput, 0, 0, NULL, 0, ShapeSet, Unsorted);
|
|
185
|
+
Pixmap mask = XCreatePixmap(d, root, width, height, 1);
|
|
186
|
+
GC shape = XCreateGC(d, mask, 0, NULL), ink = XCreateGC(d, window, 0, NULL);
|
|
187
|
+
XFontStruct *font = XLoadQueryFont(d, "fixed");
|
|
188
|
+
if (font) XSetFont(d, ink, font->fid);
|
|
189
|
+
XSetLineAttributes(d, shape, 4, LineSolid, CapRound, JoinRound);
|
|
190
|
+
fcntl(STDIN_FILENO, F_SETFL, fcntl(STDIN_FILENO, F_GETFL) | O_NONBLOCK);
|
|
191
|
+
puts("READY"); fflush(stdout);
|
|
192
|
+
char pending[512]; size_t used = 0;
|
|
193
|
+
double deadline = 0;
|
|
194
|
+
for (;;) {
|
|
195
|
+
char bytes[512]; ssize_t n = read(STDIN_FILENO, bytes, sizeof(bytes));
|
|
196
|
+
if (n == 0) break;
|
|
197
|
+
for (ssize_t i = 0; i < n; i++) {
|
|
198
|
+
if (bytes[i] != '\n') { if (used < sizeof(pending)-1) pending[used++] = bytes[i]; continue; }
|
|
199
|
+
pending[used] = 0; used = 0;
|
|
200
|
+
double px, py; unsigned timeout; int offset = 0;
|
|
201
|
+
if (sscanf(pending, "P %lf %lf %u %n", &px, &py, &timeout, &offset) != 3 || !offset) continue;
|
|
202
|
+
const char *label = pending + offset;
|
|
203
|
+
int x = (int)px, y = (int)py, len = (int)strlen(label);
|
|
204
|
+
int badge_w = font ? XTextWidth(font, label, len) + 16 : len * 6 + 16;
|
|
205
|
+
if (badge_w > width) badge_w = width;
|
|
206
|
+
int badge_x = x + 30, badge_y = y - 14;
|
|
207
|
+
if (badge_x + badge_w > width) badge_x = width - badge_w;
|
|
208
|
+
if (badge_y < 0) badge_y = 0;
|
|
209
|
+
if (badge_y + 28 > height) badge_y = height - 28;
|
|
210
|
+
XUnmapWindow(d, window);
|
|
211
|
+
XSetForeground(d, shape, 0); XFillRectangle(d, mask, shape, 0, 0, width, height);
|
|
212
|
+
XSetForeground(d, shape, 1);
|
|
213
|
+
XDrawArc(d, mask, shape, x-18, y-18, 36, 36, 0, 360*64);
|
|
214
|
+
XFillArc(d, mask, shape, x-3, y-3, 6, 6, 0, 360*64);
|
|
215
|
+
if (len) XFillRectangle(d, mask, shape, badge_x, badge_y, badge_w, 28);
|
|
216
|
+
XShapeCombineMask(d, window, ShapeBounding, 0, 0, mask, ShapeSet);
|
|
217
|
+
XMapRaised(d, window);
|
|
218
|
+
XClearWindow(d, window);
|
|
219
|
+
if (len) {
|
|
220
|
+
XSetForeground(d, ink, 0x123047);
|
|
221
|
+
XFillRectangle(d, window, ink, badge_x, badge_y, badge_w, 28);
|
|
222
|
+
XSetForeground(d, ink, 0xffffff);
|
|
223
|
+
XDrawString(d, window, ink, badge_x+8, badge_y+18, label, len);
|
|
224
|
+
}
|
|
225
|
+
XSync(d, False);
|
|
226
|
+
deadline = now() + timeout/1000.0;
|
|
227
|
+
}
|
|
228
|
+
if (deadline && now() >= deadline) { XUnmapWindow(d, window); XFlush(d); deadline = 0; }
|
|
229
|
+
usleep(5000);
|
|
230
|
+
}
|
|
231
|
+
XDestroyWindow(d, window); XFreeGC(d, shape); XFreeGC(d, ink);
|
|
232
|
+
XFreePixmap(d, mask); if (font) XFreeFont(d, font); XCloseDisplay(d);
|
|
102
233
|
return 0;
|
|
103
234
|
}
|
package/engine/src/capture.rs
CHANGED
|
@@ -13,7 +13,7 @@ use pw::spa;
|
|
|
13
13
|
use pw::spa::pod::{serialize::PodSerializer, Pod};
|
|
14
14
|
use std::io::Cursor;
|
|
15
15
|
use std::os::fd::OwnedFd;
|
|
16
|
-
use std::sync::atomic::{AtomicU64, Ordering};
|
|
16
|
+
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
|
17
17
|
use std::sync::{mpsc, Arc, Mutex};
|
|
18
18
|
use std::thread::JoinHandle;
|
|
19
19
|
use std::time::{Duration, Instant};
|
|
@@ -55,6 +55,7 @@ pub struct Capture {
|
|
|
55
55
|
quit: pw::channel::Sender<()>,
|
|
56
56
|
thread: Option<JoinHandle<()>>,
|
|
57
57
|
pub source: SelectedSource,
|
|
58
|
+
cursor_positions: Arc<AtomicBool>,
|
|
58
59
|
geometry: Arc<Mutex<Option<StreamGeometry>>>,
|
|
59
60
|
frames: Arc<AtomicU64>,
|
|
60
61
|
dropped: Arc<AtomicU64>,
|
|
@@ -63,6 +64,9 @@ pub struct Capture {
|
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
impl Capture {
|
|
67
|
+
pub fn cursor_positions(&self) -> bool {
|
|
68
|
+
self.cursor_positions.load(Ordering::Relaxed)
|
|
69
|
+
}
|
|
66
70
|
pub fn geometry(&self) -> Option<StreamGeometry> {
|
|
67
71
|
self.geometry.lock().ok().and_then(|g| g.clone())
|
|
68
72
|
}
|
|
@@ -193,16 +197,29 @@ fn buffer_pod(buffer: &mut Vec<u8>) -> Result<()> {
|
|
|
193
197
|
}
|
|
194
198
|
|
|
195
199
|
/// Start capturing `session`'s node, delivering I420 frames to `sink`.
|
|
200
|
+
#[allow(clippy::too_many_arguments)]
|
|
196
201
|
pub fn start(
|
|
197
202
|
session: PortalSession,
|
|
198
203
|
encoded_width: usize,
|
|
199
204
|
encoded_height: usize,
|
|
200
205
|
max_fps: u32,
|
|
206
|
+
cursor_sink: Option<crate::cursor::CursorSink>,
|
|
201
207
|
sink: FrameSink,
|
|
202
208
|
indicator_position: Option<Arc<Mutex<Option<(i64, i64)>>>>,
|
|
203
209
|
on_stop: Box<dyn Fn(String) + Send>,
|
|
204
210
|
) -> Result<Capture> {
|
|
205
|
-
let PortalSession {
|
|
211
|
+
let PortalSession {
|
|
212
|
+
fd,
|
|
213
|
+
source,
|
|
214
|
+
cursor_mode,
|
|
215
|
+
..
|
|
216
|
+
} = session;
|
|
217
|
+
let cursor_positions = Arc::new(AtomicBool::new(false));
|
|
218
|
+
let cursor_sink = if cursor_mode == "metadata" {
|
|
219
|
+
cursor_sink
|
|
220
|
+
} else {
|
|
221
|
+
None
|
|
222
|
+
};
|
|
206
223
|
let geometry = Arc::new(Mutex::new(None));
|
|
207
224
|
let frames = Arc::new(AtomicU64::new(0));
|
|
208
225
|
let dropped = Arc::new(AtomicU64::new(0));
|
|
@@ -210,7 +227,9 @@ pub fn start(
|
|
|
210
227
|
let (ready_tx, ready_rx) = mpsc::channel::<Result<(), String>>();
|
|
211
228
|
|
|
212
229
|
let node_id = source.node_id;
|
|
230
|
+
let source_size = (source.width.max(1) as usize, source.height.max(1) as usize);
|
|
213
231
|
let thread = {
|
|
232
|
+
let cursor_positions = cursor_positions.clone();
|
|
214
233
|
let geometry = geometry.clone();
|
|
215
234
|
let frames = frames.clone();
|
|
216
235
|
let dropped = dropped.clone();
|
|
@@ -221,9 +240,12 @@ pub fn start(
|
|
|
221
240
|
let result = run_loop(
|
|
222
241
|
fd,
|
|
223
242
|
node_id,
|
|
243
|
+
source_size,
|
|
224
244
|
encoded_width,
|
|
225
245
|
encoded_height,
|
|
226
246
|
max_fps,
|
|
247
|
+
cursor_sink,
|
|
248
|
+
cursor_positions,
|
|
227
249
|
sink,
|
|
228
250
|
indicator_position,
|
|
229
251
|
on_stop,
|
|
@@ -250,6 +272,7 @@ pub fn start(
|
|
|
250
272
|
quit,
|
|
251
273
|
thread: Some(thread),
|
|
252
274
|
source,
|
|
275
|
+
cursor_positions,
|
|
253
276
|
geometry,
|
|
254
277
|
frames,
|
|
255
278
|
dropped,
|
|
@@ -268,9 +291,12 @@ pub fn start(
|
|
|
268
291
|
fn run_loop(
|
|
269
292
|
fd: OwnedFd,
|
|
270
293
|
node_id: u32,
|
|
294
|
+
source_size: (usize, usize),
|
|
271
295
|
encoded_width: usize,
|
|
272
296
|
encoded_height: usize,
|
|
273
297
|
max_fps: u32,
|
|
298
|
+
cursor_sink: Option<crate::cursor::CursorSink>,
|
|
299
|
+
cursor_positions: Arc<AtomicBool>,
|
|
274
300
|
sink: FrameSink,
|
|
275
301
|
indicator_position: Option<Arc<Mutex<Option<(i64, i64)>>>>,
|
|
276
302
|
on_stop: Box<dyn Fn(String) + Send>,
|
|
@@ -302,6 +328,8 @@ fn run_loop(
|
|
|
302
328
|
let stream = pw::stream::StreamBox::new(&core, "desklink-capture", props)
|
|
303
329
|
.context("pw_stream_new failed")?;
|
|
304
330
|
|
|
331
|
+
let wants_cursor_meta = cursor_sink.is_some();
|
|
332
|
+
let mut cursor = cursor_sink.map(crate::cursor::Reporter::new);
|
|
305
333
|
let mut state = StreamState {
|
|
306
334
|
box_w: encoded_width,
|
|
307
335
|
box_h: encoded_height,
|
|
@@ -367,6 +395,31 @@ fn run_loop(
|
|
|
367
395
|
let Some(mut buffer) = stream.dequeue_buffer() else {
|
|
368
396
|
return;
|
|
369
397
|
};
|
|
398
|
+
if let Some(cursor) = cursor.as_mut() {
|
|
399
|
+
if let Some(meta) = buffer.find_meta::<spa::buffer::meta::MetaCursor>() {
|
|
400
|
+
cursor_positions.store(true, Ordering::Relaxed);
|
|
401
|
+
// The wrapper's size check bounds the bitmap header;
|
|
402
|
+
// never follow an unchecked bitmap offset.
|
|
403
|
+
let bitmap = buffer
|
|
404
|
+
.find_meta::<CursorWithBitmap>()
|
|
405
|
+
.filter(|m| {
|
|
406
|
+
m.cursor.bitmap_offset as usize
|
|
407
|
+
== std::mem::size_of::<spa::sys::spa_meta_cursor>()
|
|
408
|
+
})
|
|
409
|
+
.map(|m| &m.bitmap);
|
|
410
|
+
if state.logical_w == 0 || state.logical_h == 0 {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
let mut position = *meta.as_raw();
|
|
414
|
+
position.position.x = (i64::from(position.position.x) * source_size.0 as i64)
|
|
415
|
+
.div_euclid(state.logical_w as i64)
|
|
416
|
+
as i32;
|
|
417
|
+
position.position.y = (i64::from(position.position.y) * source_size.1 as i64)
|
|
418
|
+
.div_euclid(state.logical_h as i64)
|
|
419
|
+
as i32;
|
|
420
|
+
cursor.update_spa(&position, bitmap, source_size.0, source_size.1);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
370
423
|
let Some(format) = state.format else { return };
|
|
371
424
|
let datas = buffer.datas_mut();
|
|
372
425
|
if datas.is_empty() {
|
|
@@ -463,10 +516,15 @@ fn run_loop(
|
|
|
463
516
|
format_pod(&mut format_buf, max_fps)?;
|
|
464
517
|
let mut buffer_buf = Vec::new();
|
|
465
518
|
buffer_pod(&mut buffer_buf)?;
|
|
466
|
-
let mut
|
|
519
|
+
let mut cursor_buf = Vec::new();
|
|
520
|
+
cursor_pod(&mut cursor_buf)?;
|
|
521
|
+
let mut params = vec![
|
|
467
522
|
Pod::from_bytes(&format_buf).context("invalid EnumFormat pod")?,
|
|
468
523
|
Pod::from_bytes(&buffer_buf).context("invalid Buffers pod")?,
|
|
469
524
|
];
|
|
525
|
+
if wants_cursor_meta {
|
|
526
|
+
params.push(Pod::from_bytes(&cursor_buf).context("invalid Cursor Meta pod")?);
|
|
527
|
+
}
|
|
470
528
|
|
|
471
529
|
stream
|
|
472
530
|
.connect(
|
|
@@ -483,7 +541,8 @@ fn run_loop(
|
|
|
483
541
|
|
|
484
542
|
// The portal composites layer-shell into its pixels. Keep the last clean
|
|
485
543
|
// backing for the helper's bounded circle and edge strips before diff/encode;
|
|
486
|
-
// the center
|
|
544
|
+
// the center (4 logical px, which the cue leaves clear) stays live so a
|
|
545
|
+
// click marker at the caret remains observable.
|
|
487
546
|
struct IndicatorMask {
|
|
488
547
|
position: Arc<Mutex<Option<(i64, i64)>>>,
|
|
489
548
|
clean: Option<Vec<u8>>,
|
|
@@ -533,6 +592,7 @@ impl IndicatorMask {
|
|
|
533
592
|
)
|
|
534
593
|
});
|
|
535
594
|
let radius = (150 * width / source_w).max(55) as i64;
|
|
595
|
+
let hole = (4 * width / source_w) as i64;
|
|
536
596
|
let edge = (20 * width / source_w)
|
|
537
597
|
.max(8)
|
|
538
598
|
.min(width / 2)
|
|
@@ -572,7 +632,7 @@ impl IndicatorMask {
|
|
|
572
632
|
};
|
|
573
633
|
(col as i64 - cx).pow(2) + (row as i64 - cy).pow(2) <= radius * radius
|
|
574
634
|
});
|
|
575
|
-
if (distance <= radius * radius || along) && distance >
|
|
635
|
+
if (distance <= radius * radius || along) && distance > hole * hole {
|
|
576
636
|
let at = start + col * 4;
|
|
577
637
|
raw[at..at + 4].copy_from_slice(&clean[at..at + 4]);
|
|
578
638
|
}
|
|
@@ -658,6 +718,7 @@ mod tests {
|
|
|
658
718
|
origin_x: 0,
|
|
659
719
|
origin_y: 0,
|
|
660
720
|
},
|
|
721
|
+
cursor_positions: Arc::new(AtomicBool::new(false)),
|
|
661
722
|
geometry: Arc::new(Mutex::new(None)),
|
|
662
723
|
frames: Arc::new(AtomicU64::new(0)),
|
|
663
724
|
dropped: Arc::new(AtomicU64::new(0)),
|
|
@@ -678,3 +739,33 @@ mod tests {
|
|
|
678
739
|
.expect("capture stopped and joined");
|
|
679
740
|
}
|
|
680
741
|
}
|
|
742
|
+
|
|
743
|
+
fn cursor_pod(buffer: &mut Vec<u8>) -> Result<()> {
|
|
744
|
+
use spa::pod::{object, property, Value};
|
|
745
|
+
let obj = object!(
|
|
746
|
+
spa::utils::SpaTypes::ObjectParamMeta,
|
|
747
|
+
spa::param::ParamType::Meta,
|
|
748
|
+
property!(
|
|
749
|
+
RawKey(spa::sys::SPA_PARAM_META_type),
|
|
750
|
+
Id,
|
|
751
|
+
RawKey(spa::sys::SPA_META_Cursor)
|
|
752
|
+
),
|
|
753
|
+
property!(
|
|
754
|
+
RawKey(spa::sys::SPA_PARAM_META_size),
|
|
755
|
+
Int,
|
|
756
|
+
(std::mem::size_of::<CursorWithBitmap>() + 256 * 256 * 4) as i32
|
|
757
|
+
)
|
|
758
|
+
);
|
|
759
|
+
PodSerializer::serialize(Cursor::new(buffer), &Value::Object(obj))
|
|
760
|
+
.map(|_| ())
|
|
761
|
+
.map_err(|e| anyhow::anyhow!("failed to build Cursor Meta pod: {e}"))
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
#[repr(C)]
|
|
765
|
+
struct CursorWithBitmap {
|
|
766
|
+
cursor: spa::sys::spa_meta_cursor,
|
|
767
|
+
bitmap: spa::sys::spa_meta_bitmap,
|
|
768
|
+
}
|
|
769
|
+
impl spa::buffer::meta::Metadata for CursorWithBitmap {
|
|
770
|
+
const META_TYPE: u32 = spa::sys::SPA_META_Cursor;
|
|
771
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//! Windows desktop capture is deferred; encoded sources never call this adapter.
|
|
2
|
+
use crate::convert::I420;
|
|
3
|
+
use crate::portal::PortalSession;
|
|
4
|
+
use anyhow::{bail, Result};
|
|
5
|
+
|
|
6
|
+
pub type FrameSink = Box<dyn Fn(I420, u64, Pixels) + Send + 'static>;
|
|
7
|
+
pub type Pixels<'a> = &'a mut dyn FnMut() -> Option<Vec<u8>>;
|
|
8
|
+
pub struct Capture;
|
|
9
|
+
|
|
10
|
+
pub fn unavailable_reason() -> &'static str {
|
|
11
|
+
"Windows display capture is not implemented in this build."
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
pub fn start(
|
|
15
|
+
_session: PortalSession,
|
|
16
|
+
_width: usize,
|
|
17
|
+
_height: usize,
|
|
18
|
+
_fps: u32,
|
|
19
|
+
_sink: FrameSink,
|
|
20
|
+
_status: Box<dyn Fn(bool, String) + Send>,
|
|
21
|
+
) -> Result<Capture> {
|
|
22
|
+
bail!("{}", unavailable_reason())
|
|
23
|
+
}
|