@desklink/host 0.2.0 → 0.3.1

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 (82) hide show
  1. package/NOTICE +26 -0
  2. package/README.md +134 -56
  3. package/bin/desklink-host.mjs +13 -5
  4. package/dist/bridge.d.ts +1 -0
  5. package/dist/bridge.d.ts.map +1 -1
  6. package/dist/bridge.js +12 -4
  7. package/dist/bridge.js.map +1 -1
  8. package/dist/engineProcess.d.ts +6 -1
  9. package/dist/engineProcess.d.ts.map +1 -1
  10. package/dist/engineProcess.js +35 -4
  11. package/dist/engineProcess.js.map +1 -1
  12. package/dist/protocol.d.ts +58 -0
  13. package/dist/protocol.d.ts.map +1 -1
  14. package/dist/protocol.js.map +1 -1
  15. package/dist/resolveEngine.d.ts +7 -1
  16. package/dist/resolveEngine.d.ts.map +1 -1
  17. package/dist/resolveEngine.js +66 -14
  18. package/dist/resolveEngine.js.map +1 -1
  19. package/docs/BOUNDARY.md +15 -7
  20. package/docs/PROTOCOL.md +267 -27
  21. package/engine/Cargo.lock +47 -2
  22. package/engine/Cargo.toml +18 -13
  23. package/engine/build.rs +86 -4
  24. package/engine/native/agent_overlay_mac.m +51 -4
  25. package/engine/native/agent_overlay_wayland.c +145 -12
  26. package/engine/native/agent_overlay_x11.c +72 -0
  27. package/engine/native/nvenc_shim.c +453 -0
  28. package/engine/native/vaapi_shim.c +727 -0
  29. package/engine/native/vpx_shim.c +54 -4
  30. package/engine/native/vt_shim.c +448 -0
  31. package/engine/src/capture.rs +167 -58
  32. package/engine/src/capture_mac.rs +24 -14
  33. package/engine/src/capture_win.rs +23 -0
  34. package/engine/src/clipboard_win.rs +8 -0
  35. package/engine/src/convert.rs +286 -61
  36. package/engine/src/cursor.rs +118 -0
  37. package/engine/src/encoder.rs +122 -13
  38. package/engine/src/h264.rs +188 -0
  39. package/engine/src/h264_nvenc.rs +308 -0
  40. package/engine/src/h264_openh264.rs +735 -0
  41. package/engine/src/h264_vaapi.rs +263 -0
  42. package/engine/src/h264_vt.rs +368 -0
  43. package/engine/src/indicator.rs +104 -3
  44. package/engine/src/input_win.rs +91 -0
  45. package/engine/src/keymap_win.rs +39 -0
  46. package/engine/src/mac.rs +34 -9
  47. package/engine/src/main.rs +186 -26
  48. package/engine/src/peer.rs +466 -28
  49. package/engine/src/portal.rs +42 -4
  50. package/engine/src/portal_win.rs +37 -0
  51. package/engine/src/protocol.rs +64 -2
  52. package/engine/src/session.rs +1252 -105
  53. package/engine/src/win.rs +110 -0
  54. package/engine/src/x11.rs +225 -9
  55. package/engine/src/x11_mac.rs +12 -0
  56. package/engine/vendor/libva/COPYING +19 -0
  57. package/engine/vendor/libva/va/va.h +5450 -0
  58. package/engine/vendor/libva/va/va_dec_av1.h +695 -0
  59. package/engine/vendor/libva/va/va_dec_hevc.h +616 -0
  60. package/engine/vendor/libva/va/va_dec_jpeg.h +157 -0
  61. package/engine/vendor/libva/va/va_dec_vp8.h +249 -0
  62. package/engine/vendor/libva/va/va_dec_vp9.h +312 -0
  63. package/engine/vendor/libva/va/va_dec_vvc.h +688 -0
  64. package/engine/vendor/libva/va/va_drm.h +61 -0
  65. package/engine/vendor/libva/va/va_enc_av1.h +1010 -0
  66. package/engine/vendor/libva/va/va_enc_h264.h +659 -0
  67. package/engine/vendor/libva/va/va_enc_hevc.h +958 -0
  68. package/engine/vendor/libva/va/va_enc_jpeg.h +162 -0
  69. package/engine/vendor/libva/va/va_enc_mpeg2.h +308 -0
  70. package/engine/vendor/libva/va/va_enc_vp8.h +350 -0
  71. package/engine/vendor/libva/va/va_enc_vp9.h +639 -0
  72. package/engine/vendor/libva/va/va_fei.h +170 -0
  73. package/engine/vendor/libva/va/va_fei_h264.h +505 -0
  74. package/engine/vendor/libva/va/va_fei_hevc.h +254 -0
  75. package/engine/vendor/libva/va/va_prot.h +525 -0
  76. package/engine/vendor/libva/va/va_version.h +87 -0
  77. package/engine/vendor/libva/va/va_vpp.h +1590 -0
  78. package/engine/vendor/nv-codec-headers/LICENSE +24 -0
  79. package/engine/vendor/nv-codec-headers/README +7 -0
  80. package/engine/vendor/nv-codec-headers/include/ffnvcodec/nvEncodeAPI.h +3825 -0
  81. package/examples/reference-client.html +1 -1
  82. package/package.json +4 -3
package/engine/build.rs CHANGED
@@ -1,8 +1,8 @@
1
1
  //! Native dependencies of the engine.
2
2
  //!
3
- //! The Linux build uses two permissive C libraries, both built or linked from
4
- //! this machine's own sources — no prebuilt binary is fetched, and nothing is
5
- //! installed:
3
+ //! The Linux build compiles its native adapters from local sources and links
4
+ //! system libraries for desktop overlays — no prebuilt binary is fetched at
5
+ //! build time, and nothing is installed:
6
6
  //!
7
7
  //! * libvpx (BSD-3-Clause) — VP9 encode, through `native/vpx_shim.c` so the
8
8
  //! versioned encoder config struct is laid out by a C compiler.
@@ -10,6 +10,12 @@
10
10
  //! over `uinput`/`libevdev`, built by its own CMake project into a static
11
11
  //! library and called through its documented C API.
12
12
  //!
13
+ //! H.264 encoding adds no link-time dependency: NVENC and VA-API are loaded with
14
+ //! `dlopen` from the machine's driver at run time, compiled against vendored
15
+ //! MIT headers (`vendor/nv-codec-headers`, `vendor/libva`); openh264 is Cisco's
16
+ //! prebuilt library, fetched by the engine at run time (see `src/h264.rs`). The
17
+ //! macOS build links the VideoToolbox system framework.
18
+ //!
13
19
  //! macOS builds only the libvpx shim; ScreenCaptureKit, Quartz input and
14
20
  //! AppKit clipboard use native frameworks. Linux runtime input access is still
15
21
  //! checked through `/dev/uinput`.
@@ -25,6 +31,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
25
31
  println!("cargo:rustc-check-cfg=cfg(desklink_vpx)");
26
32
  println!("cargo:rustc-check-cfg=cfg(desklink_macos_cli)");
27
33
  println!("cargo:rerun-if-changed=native/vpx_shim.c");
34
+ println!("cargo:rerun-if-changed=native/nvenc_shim.c");
35
+ println!("cargo:rerun-if-changed=native/vaapi_shim.c");
36
+ println!("cargo:rerun-if-changed=native/vt_shim.c");
28
37
  println!("cargo:rerun-if-changed=native/inputtino_shim.cpp");
29
38
  println!("cargo:rerun-if-changed=native/mac_stream.mm");
30
39
  println!("cargo:rerun-if-changed=native/agent_overlay_mac.m");
@@ -39,6 +48,44 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
39
48
  println!("cargo:rerun-if-env-changed=DESKLINK_MACOS_CLI");
40
49
 
41
50
  let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
51
+ if target_os == "windows" {
52
+ if let Some(prefix) = std::env::var_os("DESKLINK_VPX_STATIC_DIR").map(PathBuf::from) {
53
+ if std::env::var("CARGO_CFG_TARGET_ENV")? != "msvc"
54
+ || std::env::var("CARGO_CFG_TARGET_ARCH")? != "x86_64"
55
+ {
56
+ return Err("Windows libvpx requires x86_64-pc-windows-msvc".into());
57
+ }
58
+ if !std::env::var("CARGO_CFG_TARGET_FEATURE")?
59
+ .split(',')
60
+ .any(|f| f == "crt-static")
61
+ {
62
+ return Err(
63
+ "Windows static libvpx requires RUSTFLAGS=-Ctarget-feature=+crt-static (/MT)"
64
+ .into(),
65
+ );
66
+ }
67
+ if !prefix.join("include/vpx/vp8cx.h").is_file()
68
+ || !prefix.join("lib/vpx.lib").is_file()
69
+ {
70
+ return Err(
71
+ "DESKLINK_VPX_STATIC_DIR must contain include/vpx/vp8cx.h and lib/vpx.lib"
72
+ .into(),
73
+ );
74
+ }
75
+ cc::Build::new()
76
+ .file("native/vpx_shim.c")
77
+ .include(prefix.join("include"))
78
+ .static_crt(true)
79
+ .compile("dlvpx");
80
+ println!(
81
+ "cargo:rustc-link-search=native={}",
82
+ prefix.join("lib").display()
83
+ );
84
+ println!("cargo:rustc-link-lib=static=vpx");
85
+ println!("cargo:rustc-cfg=desklink_vpx");
86
+ }
87
+ return Ok(());
88
+ }
42
89
  if let Some(prefix) = std::env::var_os("DESKLINK_VPX_STATIC_DIR").map(PathBuf::from) {
43
90
  if !prefix.join("include/vpx/vp8cx.h").exists() || !prefix.join("lib/libvpx.a").exists() {
44
91
  return Err(format!("DESKLINK_VPX_STATIC_DIR must be the libvpx install prefix containing include/vpx/vp8cx.h and lib/libvpx.a, not its lib directory: {}", prefix.display()).into());
@@ -69,11 +116,41 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
69
116
  }
70
117
 
71
118
  if target_os == "linux" {
119
+ // H.264 through NVENC and VA-API: the driver libraries are dlopen'd at
120
+ // run time, so building needs only their vendored MIT headers, and a
121
+ // machine without them still runs the engine.
122
+ cc::Build::new()
123
+ .file("native/nvenc_shim.c")
124
+ .include("vendor/nv-codec-headers/include")
125
+ .compile("dlnvenc");
72
126
  cc::Build::new()
127
+ .file("native/vaapi_shim.c")
128
+ .include("vendor/libva")
129
+ .compile("dlvaapi");
130
+ println!("cargo:rustc-link-lib=dl");
131
+ let mut overlay = cc::Build::new();
132
+ let includes = std::process::Command::new("pkg-config")
133
+ .args(["--cflags", "freetype2", "fontconfig"])
134
+ .output()?;
135
+ if !includes.status.success() {
136
+ return Err("FreeType and Fontconfig development headers are required".into());
137
+ }
138
+ for flag in String::from_utf8(includes.stdout)?.split_whitespace() {
139
+ overlay.flag(flag);
140
+ }
141
+ overlay
73
142
  .file("native/agent_overlay_x11.c")
74
143
  .file("native/agent_overlay_wayland.c")
75
144
  .compile("dlagentoverlay");
76
- for lib in ["X11", "Xcursor", "wayland-client", "m"] {
145
+ for lib in [
146
+ "X11",
147
+ "Xext",
148
+ "Xcursor",
149
+ "wayland-client",
150
+ "fontconfig",
151
+ "freetype",
152
+ "m",
153
+ ] {
77
154
  println!("cargo:rustc-link-lib={lib}");
78
155
  }
79
156
  cc::Build::new()
@@ -114,6 +191,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
114
191
  );
115
192
  println!("cargo:rustc-link-lib=static=vpx");
116
193
  println!("cargo:rustc-cfg=desklink_vpx");
194
+ // H.264 for receivers that decode it in hardware; VideoToolbox is a
195
+ // system framework, so linking it adds no dependency.
196
+ cc::Build::new().file("native/vt_shim.c").compile("dlvt");
197
+ println!("cargo:rustc-link-lib=framework=VideoToolbox");
198
+ println!("cargo:rustc-link-lib=framework=CoreFoundation");
117
199
  }
118
200
  println!("cargo:rustc-link-lib=framework=CoreGraphics");
119
201
  println!("cargo:rustc-link-lib=framework=ApplicationServices");
@@ -3,18 +3,31 @@
3
3
  #include <stdio.h>
4
4
  #include <stdlib.h>
5
5
  #include <unistd.h>
6
+ #include <string.h>
6
7
 
7
8
  @interface AgentOverlayView : NSView {
8
9
  NSPoint target, cursor;
9
10
  CFTimeInterval clickAt, typeAt, endingAt;
10
11
  BOOL positioned;
12
+ BOOL pointMode, pointVisible;
13
+ CFTimeInterval pointDeadline;
14
+ NSString *pointLabel;
11
15
  }
12
16
  - (void)event:(char)kind x:(double)x y:(double)y;
13
17
  - (void)end;
18
+ - (void)enablePointMode;
19
+ - (void)pointX:(double)x y:(double)y timeout:(unsigned)timeout label:(NSString *)label;
14
20
  @end
15
21
 
16
22
  @implementation AgentOverlayView
17
23
  - (BOOL)isOpaque { return NO; }
24
+ - (void)enablePointMode { pointMode = YES; }
25
+ - (void)pointX:(double)x y:(double)y timeout:(unsigned)timeout label:(NSString *)label {
26
+ cursor = NSMakePoint(x, self.bounds.size.height-y);
27
+ pointLabel = label; pointVisible = YES;
28
+ pointDeadline = CACurrentMediaTime() + timeout/1000.0;
29
+ [self setNeedsDisplay:YES];
30
+ }
18
31
  - (void)event:(char)kind x:(double)x y:(double)y {
19
32
  if (kind == 'S') { [self end]; return; }
20
33
  if (x >= 0 && y >= 0) {
@@ -27,8 +40,12 @@
27
40
  if (kind == 'T') typeAt = now;
28
41
  [self setNeedsDisplay:YES];
29
42
  }
30
- - (void)end { endingAt = CACurrentMediaTime(); }
43
+ - (void)end { if (pointMode) { [NSApp terminate:nil]; return; } endingAt = CACurrentMediaTime(); }
31
44
  - (void)tick {
45
+ if (pointMode) {
46
+ if (pointVisible && CACurrentMediaTime() >= pointDeadline) { pointVisible = NO; [self setNeedsDisplay:YES]; }
47
+ return;
48
+ }
32
49
  if (endingAt && CACurrentMediaTime() - endingAt > 0.7) { [NSApp terminate:nil]; return; }
33
50
  cursor.x += (target.x - cursor.x) * 0.24;
34
51
  cursor.y += (target.y - cursor.y) * 0.24;
@@ -36,6 +53,23 @@
36
53
  }
37
54
  - (void)drawRect:(NSRect)rect {
38
55
  [[NSColor clearColor] set]; NSRectFillUsingOperation(rect, NSCompositingOperationCopy);
56
+ if (pointMode) {
57
+ if (!pointVisible) return;
58
+ [[NSColor colorWithCalibratedRed:0.30 green:0.62 blue:0.82 alpha:1] set];
59
+ NSBezierPath *ring = [NSBezierPath bezierPathWithOvalInRect:NSMakeRect(cursor.x-18, cursor.y-18, 36, 36)];
60
+ ring.lineWidth = 4; [ring stroke];
61
+ [[NSBezierPath bezierPathWithOvalInRect:NSMakeRect(cursor.x-3,cursor.y-3,6,6)] fill];
62
+ if (pointLabel.length) {
63
+ NSDictionary *style = @{NSFontAttributeName: [NSFont systemFontOfSize:13], NSForegroundColorAttributeName: NSColor.whiteColor};
64
+ NSSize size = [pointLabel sizeWithAttributes:style];
65
+ CGFloat x = fmax(0, fmin(cursor.x+30, self.bounds.size.width-size.width-16));
66
+ CGFloat y = fmax(0, fmin(cursor.y-14, self.bounds.size.height-28));
67
+ [[NSColor colorWithCalibratedRed:0.07 green:0.19 blue:0.28 alpha:1] set];
68
+ [[NSBezierPath bezierPathWithRoundedRect:NSMakeRect(x,y,size.width+16,28) xRadius:4 yRadius:4] fill];
69
+ [pointLabel drawAtPoint:NSMakePoint(x+8,y+6) withAttributes:style];
70
+ }
71
+ return;
72
+ }
39
73
  CFTimeInterval now = CACurrentMediaTime();
40
74
  CGFloat edge = endingAt ? fmax(0, 1 - (now-endingAt)/0.7) : 1;
41
75
  if (edge > 0) {
@@ -95,7 +129,7 @@ unsigned char *desklink_image_bgra(CGImageRef image, size_t *length, size_t *str
95
129
  return pixels;
96
130
  }
97
131
 
98
- int desklink_agent_overlay_main(int display_id) {
132
+ static int overlay_main(int display_id, BOOL point_mode) {
99
133
  @autoreleasepool {
100
134
  [NSApplication sharedApplication];
101
135
  [NSApp setActivationPolicy:NSApplicationActivationPolicyProhibited];
@@ -111,14 +145,24 @@ int desklink_agent_overlay_main(int display_id) {
111
145
  window.collectionBehavior = NSWindowCollectionBehaviorCanJoinAllSpaces | NSWindowCollectionBehaviorStationary;
112
146
  AgentOverlayView *view = [[AgentOverlayView alloc] initWithFrame:NSMakeRect(0,0,screen.frame.size.width,screen.frame.size.height)];
113
147
  window.contentView = view;
148
+ if (point_mode) [view enablePointMode];
114
149
  NSPoint mouse = [NSEvent mouseLocation];
115
- [view event:'M' x:mouse.x-screen.frame.origin.x y:NSMaxY(screen.frame)-mouse.y];
150
+ if (!point_mode) [view event:'M' x:mouse.x-screen.frame.origin.x y:NSMaxY(screen.frame)-mouse.y];
116
151
  [window orderFrontRegardless];
117
152
  puts("READY"); fflush(stdout);
118
153
  [NSTimer scheduledTimerWithTimeInterval:1.0/60 target:view selector:@selector(tick) userInfo:nil repeats:YES];
119
154
  dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
120
- char line[128];
155
+ char line[512];
121
156
  while (fgets(line, sizeof(line), stdin)) {
157
+ if (point_mode) {
158
+ double x, y; unsigned timeout; int offset = 0;
159
+ if (sscanf(line, "P %lf %lf %u %n", &x, &y, &timeout, &offset) == 3 && offset) {
160
+ line[strcspn(line, "\n")] = 0;
161
+ NSString *label = [NSString stringWithUTF8String:line+offset];
162
+ dispatch_async(dispatch_get_main_queue(), ^{ [view pointX:x y:y timeout:timeout label:label]; });
163
+ }
164
+ continue;
165
+ }
122
166
  char kind; double x = 0, y = 0;
123
167
  if (sscanf(line, "%c %lf %lf", &kind, &x, &y) >= 1) {
124
168
  dispatch_async(dispatch_get_main_queue(), ^{ [view event:kind x:x y:y]; });
@@ -131,3 +175,6 @@ int desklink_agent_overlay_main(int display_id) {
131
175
  }
132
176
  return 0;
133
177
  }
178
+
179
+ int desklink_agent_overlay_main(int display_id) { return overlay_main(display_id, NO); }
180
+ int desklink_point_overlay_main(int display_id) { return overlay_main(display_id, YES); }
@@ -17,6 +17,9 @@
17
17
  #include <stdio.h>
18
18
  #include <stdlib.h>
19
19
  #include <string.h>
20
+ #include <fontconfig/fontconfig.h>
21
+ #include <ft2build.h>
22
+ #include FT_FREETYPE_H
20
23
 
21
24
  // Layer-shell v1 protocol (Drew DeVault, 2017; MIT license).
22
25
  static const struct wl_interface layer_surface_interface;
@@ -35,6 +38,38 @@ static const struct wl_message surface_requests[] = {
35
38
  static const struct wl_message surface_events[] = { {"configure", "uuu", NULL}, {"closed", "", NULL} };
36
39
  static const struct wl_interface layer_surface_interface = {"zwlr_layer_surface_v1", 1, 8, surface_requests, 2, surface_events};
37
40
 
41
+ /* xdg-output wire signatures: Copyright (c) 2017 Red Hat Inc.
42
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
43
+ * of this software and associated documentation files (the "Software"), to deal
44
+ * in the Software without restriction, including without limitation the rights
45
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
46
+ * copies of the Software, and to permit persons to whom the Software is
47
+ * furnished to do so, subject to the following conditions:
48
+ * The above copyright notice and this permission notice shall be included in
49
+ * all copies or substantial portions of the Software.
50
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
51
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
52
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
53
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
54
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
55
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
56
+ * SOFTWARE.
57
+ */
58
+ // xdg-output v2 supplies compositor logical coordinates, including scaled outputs.
59
+ static const struct wl_interface xdg_output_interface;
60
+ static const struct wl_interface *xdg_types[]={&xdg_output_interface,&wl_output_interface};
61
+ static const struct wl_message xdg_requests[]={{"destroy","",NULL},{"get_xdg_output","no",xdg_types}};
62
+ static const struct wl_interface xdg_manager_interface={"zxdg_output_manager_v1",2,2,xdg_requests,0,NULL};
63
+ static const struct wl_message output_requests[]={{"destroy","",NULL}};
64
+ static const struct wl_message output_events[]={{"logical_position","ii",NULL},{"logical_size","ii",NULL},{"done","",NULL},{"name","2s",NULL},{"description","2s",NULL}};
65
+ static const struct wl_interface xdg_output_interface={"zxdg_output_v1",2,1,output_requests,5,output_events};
66
+ struct Output { struct wl_output *handle; struct wl_proxy *logical; int x,y,w,h,valid; };
67
+ static void logical_position(void *data, struct wl_proxy *p,int32_t x,int32_t y){(void)p;struct Output *o=data;o->x=x;o->y=y;o->valid=1;}
68
+ static void logical_size(void *data,struct wl_proxy *p,int32_t w,int32_t h){(void)p;struct Output *o=data;o->w=w;o->h=h;}
69
+ static void logical_done(void *data,struct wl_proxy *p){(void)data;(void)p;}
70
+ static void logical_string(void *data,struct wl_proxy *p,const char *s){(void)data;(void)p;(void)s;}
71
+ static void (*logical_listener[])(void)={(void (*)(void))logical_position,(void (*)(void))logical_size,(void (*)(void))logical_done,(void (*)(void))logical_string,(void (*)(void))logical_string};
72
+
38
73
  int desklink_wayland_geometry(int source_w, int source_h, int surface_w, int surface_h,
39
74
  double px, double py, double *x, double *y) {
40
75
  if (source_w<=0||source_h<=0||surface_w<=0||surface_h<=0) return 0;
@@ -50,7 +85,8 @@ struct Overlay {
50
85
  struct wl_display *display;
51
86
  struct wl_compositor *compositor;
52
87
  struct wl_shm *shm;
53
- struct wl_proxy *shell, *layer;
88
+ struct wl_proxy *shell, *layer, *output_manager;
89
+ struct Output outputs[32]; int output_count;
54
90
  struct wl_surface *surface;
55
91
  struct Buffer buffers[2];
56
92
  int width, height, source_w, source_h, configured, closed;
@@ -58,6 +94,11 @@ struct Overlay {
58
94
  static double now(void) { struct timespec t; clock_gettime(CLOCK_MONOTONIC,&t); return t.tv_sec+t.tv_nsec/1e9; }
59
95
  static void registry_global(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) {
60
96
  struct Overlay *o=data;
97
+ if (!strcmp(interface,"wl_output") && o->output_count<32) {
98
+ struct Output *out=&o->outputs[o->output_count++];
99
+ out->handle=wl_registry_bind(registry,name,&wl_output_interface,1);
100
+ }
101
+ if (!strcmp(interface,"zxdg_output_manager_v1")) o->output_manager=wl_registry_bind(registry,name,&xdg_manager_interface,version<2?version:2);
61
102
  if (!strcmp(interface,"wl_compositor")) o->compositor=wl_registry_bind(registry,name,&wl_compositor_interface,version<4?version:4);
62
103
  if (!strcmp(interface,"wl_shm")) o->shm=wl_registry_bind(registry,name,&wl_shm_interface,1);
63
104
  if (!strcmp(interface,"zwlr_layer_shell_v1")) o->shell=wl_registry_bind(registry,name,&shell_interface,1);
@@ -65,7 +106,11 @@ static void registry_global(void *data, struct wl_registry *registry, uint32_t n
65
106
  static void registry_remove(void *data, struct wl_registry *registry, uint32_t name) { (void)data;(void)registry;(void)name; }
66
107
  static const struct wl_registry_listener registry_listener={registry_global,registry_remove};
67
108
  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;
109
+ struct Overlay *o=data;
110
+ // A source/layout change invalidates this helper; never write new dimensions
111
+ // into buffers allocated for the old output size.
112
+ if(o->configured && (o->width!=(int)w || o->height!=(int)h))o->closed=1;
113
+ o->width=(int)w; o->height=(int)h; o->configured=1;
69
114
  wl_proxy_marshal_flags(layer,6,NULL,1,0,serial);
70
115
  }
71
116
  static void closed(void *data, struct wl_proxy *layer) { (void)layer; ((struct Overlay *)data)->closed=1; }
@@ -117,16 +162,76 @@ static void draw(struct Overlay *o, struct Buffer *b, double x, double y, double
117
162
  }
118
163
  }
119
164
 
120
- int desklink_agent_overlay_wayland(int source_w, int source_h) {
165
+ static FT_Face point_font(FT_Library *library) {
166
+ if(FT_Init_FreeType(library))return NULL;
167
+ FcPattern *pattern=FcNameParse((const FcChar8 *)"monospace");
168
+ if(!pattern)return NULL;
169
+ FcConfigSubstitute(NULL,pattern,FcMatchPattern);FcDefaultSubstitute(pattern);
170
+ FcResult result;FcPattern *match=FcFontMatch(NULL,pattern,&result);FcPatternDestroy(pattern);
171
+ FcChar8 *path=NULL;FT_Face face=NULL;
172
+ if(match && FcPatternGetString(match,FC_FILE,0,&path)==FcResultMatch)
173
+ if(!FT_New_Face(*library,(const char *)path,0,&face))FT_Set_Pixel_Sizes(face,0,13);
174
+ if(match)FcPatternDestroy(match);
175
+ return face;
176
+ }
177
+ static void draw_point(struct Overlay *o,struct Buffer *b,double x,double y,const char *label,FT_Face font,int visible) {
178
+ memset(b->pixels,0,b->length);if(!visible)return;
179
+ int w=o->width,h=o->height;
180
+ for(int py=fmax(0,y-21);py<fmin(h,y+22);py++)for(int px=fmax(0,x-21);px<fmin(w,x+22);px++) {
181
+ double d=hypot(px-x,py-y);
182
+ if(fabs(d-18)<=2 || d<=3)b->pixels[(size_t)py*w+px]=0xff4c9ed0;
183
+ }
184
+ if(!*label)return;
185
+ int badge_w=16;
186
+ for(const char *p=label;*p;p++)if(!FT_Load_Char(font,(unsigned char)*p,FT_LOAD_DEFAULT))badge_w+=font->glyph->advance.x>>6;
187
+ badge_w=fmin(w,badge_w);int bx=fmax(0,fmin(x+30,w-badge_w)),by=fmax(0,fmin(y-14,h-28));
188
+ for(int py=by;py<fmin(h,by+28);py++)for(int px=bx;px<bx+badge_w;px++)b->pixels[(size_t)py*w+px]=0xff123047;
189
+ int pen=bx+8;
190
+ for(const char *p=label;*p;p++)if(!FT_Load_Char(font,(unsigned char)*p,FT_LOAD_RENDER)) {
191
+ FT_GlyphSlot g=font->glyph;
192
+ for(unsigned row=0;row<g->bitmap.rows;row++)for(unsigned col=0;col<g->bitmap.width;col++) {
193
+ int px=pen+g->bitmap_left+col,py=by+18-g->bitmap_top+row;
194
+ if(px<0||px>=w||py<0||py>=h)continue;
195
+ unsigned a=g->bitmap.buffer[row*g->bitmap.pitch+col];
196
+ unsigned r=(18*(255-a)+255*a)/255,green=(48*(255-a)+255*a)/255,blue=(71*(255-a)+255*a)/255;
197
+ b->pixels[(size_t)py*w+px]=0xff000000|(r<<16)|(green<<8)|blue;
198
+ }
199
+ pen+=g->advance.x>>6;
200
+ }
201
+ }
202
+
203
+ static int overlay_main(int source_w,int source_h,int point_mode,int origin_known,int origin_x,int origin_y) {
204
+
121
205
  struct Overlay o={.source_w=source_w,.source_h=source_h};
122
206
  o.display=wl_display_connect(NULL);
123
207
  if(!o.display)return 2;
124
208
  struct wl_registry *registry=wl_display_get_registry(o.display);
125
209
  wl_registry_add_listener(registry,&registry_listener,&o);
126
210
  wl_display_roundtrip(o.display);
127
- if(!o.compositor||!o.shm||!o.shell){wl_display_disconnect(o.display);return 2;}
211
+ if(!o.shell){puts("layer_shell_unavailable");fflush(stdout);wl_display_disconnect(o.display);return 2;}
212
+ if(!o.compositor||!o.shm){wl_display_disconnect(o.display);return 2;}
213
+ struct wl_output *selected=NULL;int matches=0;
214
+ if(point_mode) {
215
+ if(o.output_manager) {
216
+ for(int i=0;i<o.output_count;i++) {
217
+ struct Output *out=&o.outputs[i];
218
+ out->logical=wl_proxy_marshal_flags(o.output_manager,1,&xdg_output_interface,wl_proxy_get_version(o.output_manager),0,NULL,out->handle);
219
+ wl_proxy_add_listener(out->logical,logical_listener,out);
220
+ }
221
+ if(wl_display_roundtrip(o.display)<0)return 2;
222
+ }
223
+ for(int i=0;i<o.output_count;i++) {
224
+ struct Output *out=&o.outputs[i];
225
+ if(origin_known && out->valid && origin_x>=out->x && origin_y>=out->y && origin_x<out->x+out->w && origin_y<out->y+out->h){selected=out->handle;matches++;}
226
+ }
227
+ if(matches>1)selected=NULL;
228
+ if(!selected && o.output_count==1 && (!origin_known || !o.output_manager))selected=o.outputs[0].handle;
229
+ if(!selected){puts("selected_output_unavailable");fflush(stdout);wl_display_disconnect(o.display);return 2;}
230
+ }
231
+ FT_Library library=NULL;FT_Face font=point_mode?point_font(&library):NULL;
232
+ if(point_mode&&!font){wl_display_disconnect(o.display);return 2;}
128
233
  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");
234
+ o.layer=wl_proxy_marshal_flags(o.shell,0,&layer_surface_interface,1,0,NULL,o.surface,selected,3,point_mode?"desklink-point":"desklink-agent");
130
235
  wl_proxy_add_listener(o.layer,layer_listener,&o);
131
236
  uint32_t anchor,keyboard_focus,empty_input;
132
237
  desklink_wayland_policy(&anchor,&keyboard_focus,&empty_input);
@@ -147,7 +252,8 @@ int desklink_agent_overlay_wayland(int source_w, int source_h) {
147
252
  puts("READY");fflush(stdout);
148
253
  double tx=0,ty=0,x=0,y=0,click=0,typed=0,ending=0;
149
254
  int active=0,positioned=0;
150
- char pending[128];size_t used=0;
255
+ double deadline=0;char label[97]={0};int dirty=0,visible=0;
256
+ char pending[512];size_t used=0;
151
257
  while(!o.closed && (!ending||now()-ending<0.6)) {
152
258
  struct pollfd fds[2]={{wl_display_get_fd(o.display),POLLIN,0},{STDIN_FILENO,POLLIN|POLLHUP,0}};
153
259
  wl_display_flush(o.display);
@@ -155,28 +261,39 @@ int desklink_agent_overlay_wayland(int source_w, int source_h) {
155
261
  if(fds[0].revents&POLLIN && wl_display_dispatch(o.display)<0)break;
156
262
  else wl_display_dispatch_pending(o.display);
157
263
  char bytes[128];ssize_t n=read(STDIN_FILENO,bytes,sizeof(bytes));
158
- if(n==0&&!ending)ending=now();
264
+ if(n==0){if(point_mode)break;if(!ending)ending=now();}
159
265
  for(ssize_t i=0;i<n;i++) {
160
266
  if(bytes[i]=='\n') {
161
267
  pending[used]=0;char kind=0;double px=0,py=0;
162
268
  if(sscanf(pending,"%c %lf %lf",&kind,&px,&py)>=1) {
163
- if(kind=='S')ending=now();
269
+ if(point_mode && kind=='P') {
270
+ unsigned timeout=0;int offset=0;
271
+ if(sscanf(pending,"P %lf %lf %u %n",&px,&py,&timeout,&offset)==3 && offset && timeout>=1 && timeout<=120000 && strlen(pending+offset)<=96) {
272
+ desklink_wayland_geometry(o.source_w,o.source_h,o.width,o.height,px,py,&x,&y);
273
+ snprintf(label,sizeof(label),"%s",pending+offset);deadline=now()+timeout/1000.0;active=1;dirty=1;visible=1;
274
+ }
275
+ }
276
+ else if(kind=='S')ending=now();
164
277
  else if(kind=='M'||kind=='C'||kind=='T'||kind=='A') {
165
278
  if(px>=0&&py>=0) {
166
279
  if(desklink_wayland_geometry(o.source_w,o.source_h,o.width,o.height,px,py,&tx,&ty)
167
280
  && !positioned){x=tx;y=ty;positioned=1;}
168
281
  }
169
282
  active=1;
170
- if(kind=='C')click=now();if(kind=='T')typed=now();
283
+ if(kind=='C')click=now();
284
+ if(kind=='T')typed=now();
171
285
  }
172
286
  }
173
287
  used=0;
174
288
  }else if(used<sizeof(pending)-1)pending[used++]=bytes[i];
175
289
  }
176
- if(!active)continue;
177
- x+=(tx-x)*0.24;y+=(ty-y)*0.24;
290
+ if(point_mode && visible && now()>=deadline){visible=0;dirty=1;}
291
+ if(!active || (point_mode&&!dirty))continue;
292
+ if(!point_mode){x+=(tx-x)*0.24;y+=(ty-y)*0.24;}
178
293
  for(int i=0;i<2;i++)if(!o.buffers[i].busy) {
179
- draw(&o,&o.buffers[i],x,y,click,typed,ending,positioned);
294
+ if(point_mode)draw_point(&o,&o.buffers[i],x,y,label,font,visible);
295
+ else draw(&o,&o.buffers[i],x,y,click,typed,ending,positioned);
296
+ dirty=0;
180
297
  o.buffers[i].busy=1;
181
298
  wl_surface_attach(o.surface,o.buffers[i].handle,0,0);
182
299
  wl_surface_damage_buffer(o.surface,0,0,o.width,o.height);
@@ -187,6 +304,22 @@ int desklink_agent_overlay_wayland(int source_w, int source_h) {
187
304
  wl_surface_attach(o.surface,NULL,0,0);wl_surface_commit(o.surface);wl_display_roundtrip(o.display);
188
305
  for(int i=0;i<2;i++){wl_buffer_destroy(o.buffers[i].handle);munmap(o.buffers[i].pixels,o.buffers[i].length);}
189
306
  wl_proxy_destroy(o.layer);wl_surface_destroy(o.surface);wl_proxy_destroy(o.shell);
307
+ if(font)FT_Done_Face(font);
308
+ if(library)FT_Done_FreeType(library);
190
309
  wl_display_disconnect(o.display);
191
310
  return 0;
192
311
  }
312
+
313
+ int desklink_agent_overlay_wayland(int w,int h){return overlay_main(w,h,0,0,0,0);}
314
+ int desklink_point_overlay_wayland(int w,int h,int known,int x,int y){return overlay_main(w,h,1,known,x,y);}
315
+
316
+ // Read-only capability probe for idempotent clear before a helper exists.
317
+ int desklink_wayland_layer_shell_available(void) {
318
+ struct Overlay o={0};o.display=wl_display_connect(NULL);
319
+ if(!o.display)return -1;
320
+ struct wl_registry *registry=wl_display_get_registry(o.display);
321
+ wl_registry_add_listener(registry,&registry_listener,&o);
322
+ int ok=wl_display_roundtrip(o.display)>=0;
323
+ int result=ok?(o.shell!=NULL):-1;
324
+ wl_display_disconnect(o.display);return result;
325
+ }
@@ -101,3 +101,75 @@ int desklink_agent_overlay_main(const char *display_name) {
101
101
  XcursorImageDestroy(image); XCloseDisplay(d);
102
102
  return 0;
103
103
  }
104
+
105
+ // Point cues are independent of the real cursor. A shaped override-redirect
106
+ // window works without a compositor; an empty input shape passes every event
107
+ // through and mapping it never asks for keyboard focus. It intentionally appears
108
+ // in GetImage(root), so the local person and the video observer share the cue.
109
+ #include <X11/extensions/shape.h>
110
+ #include <stdlib.h>
111
+
112
+ int desklink_point_overlay_main(const char *display_name) {
113
+ Display *d = XOpenDisplay(display_name);
114
+ if (!d) return 2;
115
+ int event_base, error_base;
116
+ if (!XShapeQueryExtension(d, &event_base, &error_base)) { XCloseDisplay(d); return 2; }
117
+ int screen = DefaultScreen(d), width = DisplayWidth(d, screen), height = DisplayHeight(d, screen);
118
+ Window root = RootWindow(d, screen);
119
+ XSetWindowAttributes attrs = {0};
120
+ attrs.override_redirect = True;
121
+ attrs.background_pixel = 0x4c9ed0;
122
+ Window window = XCreateWindow(d, root, 0, 0, width, height, 0,
123
+ CopyFromParent, InputOutput, CopyFromParent, CWOverrideRedirect | CWBackPixel, &attrs);
124
+ XStoreName(d, window, "Desklink point");
125
+ XShapeCombineRectangles(d, window, ShapeInput, 0, 0, NULL, 0, ShapeSet, Unsorted);
126
+ Pixmap mask = XCreatePixmap(d, root, width, height, 1);
127
+ GC shape = XCreateGC(d, mask, 0, NULL), ink = XCreateGC(d, window, 0, NULL);
128
+ XFontStruct *font = XLoadQueryFont(d, "fixed");
129
+ if (font) XSetFont(d, ink, font->fid);
130
+ XSetLineAttributes(d, shape, 4, LineSolid, CapRound, JoinRound);
131
+ fcntl(STDIN_FILENO, F_SETFL, fcntl(STDIN_FILENO, F_GETFL) | O_NONBLOCK);
132
+ puts("READY"); fflush(stdout);
133
+ char pending[512]; size_t used = 0;
134
+ double deadline = 0;
135
+ for (;;) {
136
+ char bytes[512]; ssize_t n = read(STDIN_FILENO, bytes, sizeof(bytes));
137
+ if (n == 0) break;
138
+ for (ssize_t i = 0; i < n; i++) {
139
+ if (bytes[i] != '\n') { if (used < sizeof(pending)-1) pending[used++] = bytes[i]; continue; }
140
+ pending[used] = 0; used = 0;
141
+ double px, py; unsigned timeout; int offset = 0;
142
+ if (sscanf(pending, "P %lf %lf %u %n", &px, &py, &timeout, &offset) != 3 || !offset) continue;
143
+ const char *label = pending + offset;
144
+ int x = (int)px, y = (int)py, len = (int)strlen(label);
145
+ int badge_w = font ? XTextWidth(font, label, len) + 16 : len * 6 + 16;
146
+ if (badge_w > width) badge_w = width;
147
+ int badge_x = x + 30, badge_y = y - 14;
148
+ if (badge_x + badge_w > width) badge_x = width - badge_w;
149
+ if (badge_y < 0) badge_y = 0;
150
+ if (badge_y + 28 > height) badge_y = height - 28;
151
+ XUnmapWindow(d, window);
152
+ XSetForeground(d, shape, 0); XFillRectangle(d, mask, shape, 0, 0, width, height);
153
+ XSetForeground(d, shape, 1);
154
+ XDrawArc(d, mask, shape, x-18, y-18, 36, 36, 0, 360*64);
155
+ XFillArc(d, mask, shape, x-3, y-3, 6, 6, 0, 360*64);
156
+ if (len) XFillRectangle(d, mask, shape, badge_x, badge_y, badge_w, 28);
157
+ XShapeCombineMask(d, window, ShapeBounding, 0, 0, mask, ShapeSet);
158
+ XMapRaised(d, window);
159
+ XClearWindow(d, window);
160
+ if (len) {
161
+ XSetForeground(d, ink, 0x123047);
162
+ XFillRectangle(d, window, ink, badge_x, badge_y, badge_w, 28);
163
+ XSetForeground(d, ink, 0xffffff);
164
+ XDrawString(d, window, ink, badge_x+8, badge_y+18, label, len);
165
+ }
166
+ XSync(d, False);
167
+ deadline = now() + timeout/1000.0;
168
+ }
169
+ if (deadline && now() >= deadline) { XUnmapWindow(d, window); XFlush(d); deadline = 0; }
170
+ usleep(5000);
171
+ }
172
+ XDestroyWindow(d, window); XFreeGC(d, shape); XFreeGC(d, ink);
173
+ XFreePixmap(d, mask); if (font) XFreeFont(d, font); XCloseDisplay(d);
174
+ return 0;
175
+ }