@desklink/host 0.4.0 → 0.5.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.
package/README.md CHANGED
@@ -126,10 +126,13 @@ libvpx's licence and patent grant in `prefix/THIRD_PARTY_LICENSES.txt`.
126
126
  Without that prefix the compile seam still builds and refuses `serve`.
127
127
  Hosted Windows CI resolves the opt-in engine through the published Node API
128
128
  and completes a `serve` hello/capabilities handshake with clean shutdown; it
129
- also checks Chrome decode and input relay, and DLL
130
- dependencies. Dedicated Windows rig qualification of
131
- capture, input, mixed DPI, GPU encoding and elevation is pending; signing and
132
- packaging are later lanes.
129
+ also installs the packed packages into an empty project, checks the installed
130
+ `@desklink/host` tree against its pack manifest, starts the engine from that
131
+ install, and checks Chrome decode and input relay, and DLL
132
+ dependencies. That install proof is installation and startup on a hosted
133
+ machine: the engine still comes from a source build, because Windows platform
134
+ packages are not published, and capture, input, encoding quality, mixed DPI, GPU
135
+ encoding and elevation stay with the dedicated rig lanes.
133
136
 
134
137
  ### macOS (preview)
135
138
 
@@ -276,11 +279,13 @@ await client.addCandidate(session.sessionId, session.generation, candidate, sdpM
276
279
  ```
277
280
 
278
281
  For cursor-free recordings, open with `cursor: 'hidden'` and inspect
279
- `session.cursor`: Linux portals require Metadata for hidden capture;
280
- `positions` is true for accepted hidden sessions. Subscribe to
281
- `session.cursor` events for source-coordinate `x`, `y`, `visible`, and monotonic
282
- `timestamp_us` (the same clock as local frame metadata). Hidden-only portals
283
- return `cursor_positions_unavailable`. X11 reports positions even on still
282
+ `session.cursor`. On Wayland the portal leaves the cursor out of the pixels and
283
+ the compositor supplies its position through `ext-image-copy-capture-v1`;
284
+ `positions` is true once it has. Subscribe to `session.cursor` events for
285
+ source-coordinate `x`, `y`, `visible`, the cursor image's `hotspot` where
286
+ known, and monotonic `timestamp_us` (the same clock as local frame metadata).
287
+ A compositor without that protocol still captures, cursor hidden, and says so
288
+ in `cursor.limitation`. X11 reports positions even on still
284
289
  frames and already captures root pixels without a cursor. macOS, Windows, and consumer-encoded
285
290
  sources return `cursor-unavailable` for hidden requests. Omitting the option
286
291
  preserves existing behavior. See [the cursor protocol](docs/PROTOCOL.md#cursor-free-capture).
@@ -32,7 +32,7 @@ export interface EngineCapabilities {
32
32
  cursor_modes?: Array<'embedded' | 'hidden'>;
33
33
  cursor_positions?: {
34
34
  x11: boolean;
35
- portal: 'negotiated';
35
+ wayland: 'ext-image-copy-capture-v1';
36
36
  };
37
37
  audio: boolean;
38
38
  displays?: Array<{
@@ -110,6 +110,10 @@ export type SourceRequest = {
110
110
  export interface CursorInfo {
111
111
  mode: 'embedded' | 'hidden' | 'metadata' | 'unavailable';
112
112
  positions: boolean;
113
+ /** Where hidden-cursor positions come from on Wayland. */
114
+ source?: 'ext-image-copy-capture-v1';
115
+ /** Why a hidden session has no cursor positions (yet). */
116
+ limitation?: string;
113
117
  }
114
118
  export interface OpenSessionRequest {
115
119
  /** Cursor-free frames with independent source-coordinate position events. */
@@ -164,6 +168,10 @@ export type EngineEvent = {
164
168
  y: number;
165
169
  visible: boolean;
166
170
  timestamp_us: number;
171
+ hotspot?: {
172
+ x: number;
173
+ y: number;
174
+ };
167
175
  };
168
176
  } | {
169
177
  event: 'session.description';
@@ -1 +1 @@
1
- {"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"protocol.js","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC;AA6JlC,wEAAwE;AACxE,MAAM,OAAO,aAAc,SAAQ,KAAK;IAC3B,IAAI,CAAS;IAEtB,YAAY,IAAY,EAAE,OAAe;QACrC,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC;QAC5B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACrB,CAAC;CACJ"}
1
+ {"version":3,"file":"protocol.js","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC;AAiKlC,wEAAwE;AACxE,MAAM,OAAO,aAAc,SAAQ,KAAK;IAC3B,IAAI,CAAS;IAEtB,YAAY,IAAY,EAAE,OAAe;QACrC,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC;QAC5B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACrB,CAAC;CACJ"}
package/docs/PROTOCOL.md CHANGED
@@ -126,7 +126,7 @@ require fresh consent. The macOS engine requires a VP9-enabled build
126
126
  "formats": ["bgrx", "bgra", "rgbx", "rgba"],
127
127
  "cursor": "embedded",
128
128
  "cursor_modes": ["embedded", "hidden"],
129
- "cursor_positions": {"x11": true, "portal": "negotiated"},
129
+ "cursor_positions": {"x11": true, "wayland": "ext-image-copy-capture-v1"},
130
130
  "audio": false
131
131
  },
132
132
  "encode": {"codecs": ["vp9", "h264"], "hardware": false,
@@ -450,8 +450,9 @@ runs at most about 60 ms of its rate target ahead of it, so frames never queue
450
450
  behind a burst. `refined_frames` counts refinement passes: a
451
451
  desktop that stops changing is coded once more at a fine quantizer, so it reads
452
452
  sharp. `halved_frames` counts motion frames coded at half width and height: when
453
- full-size frames that change much of the picture take longer to code than a
454
- frame interval while a newer frame is already waiting, several frames in a row,
453
+ full-size frames that change much of the picture take longer to code or to
454
+ drain at the current rate target than a frame interval while a newer frame is
455
+ already waiting or send-budget debt is held, several frames in a row,
455
456
  motion is coded at half size until the desktop stops or its changes shrink to
456
457
  small ones that code in time; the refinement pass is always full size. Key
457
458
  frames are sent only for the first frame, when the receiver asks (RTCP PLI or
@@ -459,7 +460,9 @@ FIR), and when the coded size changes that way, so a receiver never has to scale
459
460
  references across a size change. `target_kbps` is the current rate target after
460
461
  any back-off: for a standing queue on the path, measured from the receiver's
461
462
  transport-wide congestion feedback (RTP `transport-cc`), and for the loss
462
- its receiver reports carry. `codec` is what frames go out in
463
+ its receiver reports carry. It starts at the requested ceiling capped at
464
+ 3000 kbps on an unknown link, then grows toward the ceiling on clean
465
+ loss/delay feedback. `codec` is what frames go out in
463
466
  (`vp9` or `h264`) and `encoder` what codes them (`libvpx`, `videotoolbox`,
464
467
  `nvenc`, `vaapi` or `openh264`); both are empty for an encoded source.
465
468
 
@@ -490,40 +493,52 @@ whose accounting is wrong.
490
493
  ## Cursor-free capture
491
494
 
492
495
  `session.open` accepts `cursor: "hidden"` (default `"embedded"`). Existing
493
- callers keep their capture behavior. Linux hidden capture requires portal
494
- **Metadata** so cursor positions remain available while cursor pixels are
495
- omitted. A portal without Metadata returns the typed
496
- `cursor_positions_unavailable` refusal. Successful open returns
496
+ callers keep their capture behavior. Linux portal capture asks the portal for
497
+ **Hidden** (or **Metadata**, whose cursor data is ignored) so cursor pixels are
498
+ omitted; a portal that offers neither refuses with `cursor-unavailable`. Cursor
499
+ positions do not come from the portal: the engine asks the compositor directly
500
+ through the `ext-image-copy-capture-v1` pointer cursor session over the
501
+ selected output (Hyprland and wlroots compositors advertise it). Successful
502
+ open returns
497
503
  `cursor: {"mode":"metadata"|"hidden"|"embedded"|"unavailable",
498
- "positions":true|false}`; `mode` names the portal mode accepted by the portal,
499
- not just the requested option.
504
+ "positions":true|false, "source"?:"ext-image-copy-capture-v1", "limitation"?:string}`;
505
+ `mode` names the portal mode accepted by the portal, not just the requested
506
+ option.
500
507
 
501
508
  Linux capabilities advertise `capture.cursor_modes: ["embedded","hidden"]`
502
- and `capture.cursor_positions: {"x11":true,"portal":"negotiated"}`. Portal
503
- support is resolved at open: hidden capture requires Metadata and an observed
504
- SPA cursor metadata buffer. A portal that offers only Hidden refuses open with
505
- `cursor_positions_unavailable`; accepted hidden sessions report `positions:true`.
506
- Metadata-mode buffers that contain no new cursor data do not fabricate a move
507
- or a visibility change.
509
+ and `capture.cursor_positions: {"x11":true,"wayland":"ext-image-copy-capture-v1"}`.
510
+ A hidden session never fails for want of positions. `positions` is true once
511
+ the compositor delivered one within a second of open. Otherwise the stream
512
+ still runs, cursor hidden, and `limitation` says why: the compositor lacks the
513
+ protocol, or it has sent nothing yet (the cursor is off the captured output,
514
+ the seat has no pointer, or the compositor withholds positions; Hyprland
515
+ gates them on its cursor permission). The engine logs a warning in each case
516
+ and never reports a guessed or zero position.
517
+
518
+ The compositor sends a position only when it changes and only while the
519
+ cursor is over the source, at its own commit cadence. The engine keeps the last
520
+ one: a quiet compositor is a still cursor. Leaving the source reports the last
521
+ position with `visible:false`.
508
522
 
509
523
  Hidden sessions with positions emit local protocol events independently of
510
524
  pixel damage, including on a still X11 desktop:
511
525
 
512
526
  ```json
513
- {"event":"session.cursor","params":{"sessionId":"…","x":400,"y":300,"visible":true,"timestamp_us":123456}}
527
+ {"event":"session.cursor","params":{"sessionId":"…","x":400,"y":300,"visible":true,"hotspot":{"x":4,"y":6},"timestamp_us":123456}}
514
528
  ```
515
529
 
516
530
  `x,y` name the pointer hotspot in the selected source's coordinates
517
531
  (`geometry.source`), before video scaling, without the desktop origin added.
518
- Portal buffer coordinates are mapped to that geometry. `visible` is false
519
- outside the source or when SPA supplies an invisible cursor bitmap.
532
+ Compositor buffer coordinates are mapped to that geometry (Hyprland's, which
533
+ are already logical, are taken as they are). `hotspot`, where the compositor
534
+ reports it, is the hotspot's offset inside the cursor image, in the same
535
+ units. `visible` is false while the cursor is outside the source.
520
536
  `timestamp_us` is a monotonic engine-process clock in microseconds, sampled
521
- when the capture thread observes the pointer. The same clock stamps
537
+ when the engine observes the pointer. The same clock stamps
522
538
  `session.frame.changed` and `session.frame` metadata, so local recording
523
539
  consumers can compare cursor samples to frame handoff times. These are not
524
- wall-clock or RTP timestamps. SPA cursor updates accompany PipeWire buffers;
525
- X11 pointer queries run at the session frame cap even with no pixel damage.
526
- Repeated identical positions/visibility are suppressed.
540
+ wall-clock or RTP timestamps. X11 pointer queries run at the session frame cap
541
+ even with no pixel damage. Repeated identical samples are suppressed.
527
542
 
528
543
  X11 root GetImage/MIT-SHM never contains the server cursor and this engine
529
544
  never composites it, including for existing embedded-default callers. Its
package/engine/Cargo.lock CHANGED
@@ -536,7 +536,7 @@ checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
536
536
 
537
537
  [[package]]
538
538
  name = "desklink-host"
539
- version = "0.4.0"
539
+ version = "0.5.1"
540
540
  dependencies = [
541
541
  "anyhow",
542
542
  "ashpd",
@@ -563,6 +563,7 @@ dependencies = [
563
563
  "tokio",
564
564
  "wayland-client",
565
565
  "wayland-protocols",
566
+ "wayland-protocols-wlr",
566
567
  "webrtc",
567
568
  "windows-sys 0.61.2",
568
569
  "wl-clipboard-rs",
package/engine/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "desklink-host"
3
- version = "0.4.0"
3
+ version = "0.5.1"
4
4
  edition = "2021"
5
5
  rust-version = "1.97.1"
6
6
  license = "Apache-2.0"
@@ -38,11 +38,14 @@ wl-clipboard-rs = "0.9"
38
38
  openh264-sys2 = { version = "0.9.8", default-features = false, features = ["libloading"] }
39
39
  sha2 = "0.10"
40
40
  bzip2-rs = "0.1.2"
41
+ # Hidden-cursor positions (ext-image-copy-capture-v1, a staging protocol);
42
+ # already in the tree through wl-clipboard-rs.
43
+ wayland-client = "0.31"
44
+ wayland-protocols = { version = "0.32", features = ["client", "staging"] }
41
45
 
42
46
  [target.'cfg(target_os = "linux")'.dev-dependencies]
43
- # The Wayland stamp example only; already in the tree through wl-clipboard-rs.
44
- wayland-client = "0.31"
45
- wayland-protocols = { version = "0.32", features = ["client"] }
47
+ # The Wayland stamp example's lab pointer; already in the tree through wl-clipboard-rs.
48
+ wayland-protocols-wlr = { version = "0.3", features = ["client"] }
46
49
 
47
50
  [target.'cfg(target_os = "windows")'.dependencies]
48
51
  windows-sys = "0.61.2"
@@ -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::{AtomicBool, AtomicU64, Ordering};
16
+ use std::sync::atomic::{AtomicU64, Ordering};
17
17
  use std::sync::{mpsc, Arc, Mutex};
18
18
  use std::thread::JoinHandle;
19
19
  use std::time::{Duration, Instant};
@@ -55,7 +55,9 @@ 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
+ /// Hidden-cursor positions, when the compositor supplies them; kept with
59
+ /// the capture so both stop together.
60
+ pub cursor: Option<crate::wl_cursor::Tracker>,
59
61
  geometry: Arc<Mutex<Option<StreamGeometry>>>,
60
62
  frames: Arc<AtomicU64>,
61
63
  dropped: Arc<AtomicU64>,
@@ -64,9 +66,6 @@ pub struct Capture {
64
66
  }
65
67
 
66
68
  impl Capture {
67
- pub fn cursor_positions(&self) -> bool {
68
- self.cursor_positions.load(Ordering::Relaxed)
69
- }
70
69
  pub fn geometry(&self) -> Option<StreamGeometry> {
71
70
  self.geometry.lock().ok().and_then(|g| g.clone())
72
71
  }
@@ -203,23 +202,11 @@ pub fn start(
203
202
  encoded_width: usize,
204
203
  encoded_height: usize,
205
204
  max_fps: u32,
206
- cursor_sink: Option<crate::cursor::CursorSink>,
207
205
  sink: FrameSink,
208
206
  indicator_position: Option<Arc<Mutex<Option<(i64, i64)>>>>,
209
207
  on_stop: Box<dyn Fn(String) + Send>,
210
208
  ) -> Result<Capture> {
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
- };
209
+ let PortalSession { fd, source, .. } = session;
223
210
  let geometry = Arc::new(Mutex::new(None));
224
211
  let frames = Arc::new(AtomicU64::new(0));
225
212
  let dropped = Arc::new(AtomicU64::new(0));
@@ -227,9 +214,7 @@ pub fn start(
227
214
  let (ready_tx, ready_rx) = mpsc::channel::<Result<(), String>>();
228
215
 
229
216
  let node_id = source.node_id;
230
- let source_size = (source.width.max(1) as usize, source.height.max(1) as usize);
231
217
  let thread = {
232
- let cursor_positions = cursor_positions.clone();
233
218
  let geometry = geometry.clone();
234
219
  let frames = frames.clone();
235
220
  let dropped = dropped.clone();
@@ -240,12 +225,9 @@ pub fn start(
240
225
  let result = run_loop(
241
226
  fd,
242
227
  node_id,
243
- source_size,
244
228
  encoded_width,
245
229
  encoded_height,
246
230
  max_fps,
247
- cursor_sink,
248
- cursor_positions,
249
231
  sink,
250
232
  indicator_position,
251
233
  on_stop,
@@ -272,7 +254,7 @@ pub fn start(
272
254
  quit,
273
255
  thread: Some(thread),
274
256
  source,
275
- cursor_positions,
257
+ cursor: None,
276
258
  geometry,
277
259
  frames,
278
260
  dropped,
@@ -291,12 +273,9 @@ pub fn start(
291
273
  fn run_loop(
292
274
  fd: OwnedFd,
293
275
  node_id: u32,
294
- source_size: (usize, usize),
295
276
  encoded_width: usize,
296
277
  encoded_height: usize,
297
278
  max_fps: u32,
298
- cursor_sink: Option<crate::cursor::CursorSink>,
299
- cursor_positions: Arc<AtomicBool>,
300
279
  sink: FrameSink,
301
280
  indicator_position: Option<Arc<Mutex<Option<(i64, i64)>>>>,
302
281
  on_stop: Box<dyn Fn(String) + Send>,
@@ -328,8 +307,6 @@ fn run_loop(
328
307
  let stream = pw::stream::StreamBox::new(&core, "desklink-capture", props)
329
308
  .context("pw_stream_new failed")?;
330
309
 
331
- let wants_cursor_meta = cursor_sink.is_some();
332
- let mut cursor = cursor_sink.map(crate::cursor::Reporter::new);
333
310
  let mut state = StreamState {
334
311
  box_w: encoded_width,
335
312
  box_h: encoded_height,
@@ -395,31 +372,6 @@ fn run_loop(
395
372
  let Some(mut buffer) = stream.dequeue_buffer() else {
396
373
  return;
397
374
  };
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
- }
423
375
  let Some(format) = state.format else { return };
424
376
  let datas = buffer.datas_mut();
425
377
  if datas.is_empty() {
@@ -516,15 +468,10 @@ fn run_loop(
516
468
  format_pod(&mut format_buf, max_fps)?;
517
469
  let mut buffer_buf = Vec::new();
518
470
  buffer_pod(&mut buffer_buf)?;
519
- let mut cursor_buf = Vec::new();
520
- cursor_pod(&mut cursor_buf)?;
521
471
  let mut params = vec![
522
472
  Pod::from_bytes(&format_buf).context("invalid EnumFormat pod")?,
523
473
  Pod::from_bytes(&buffer_buf).context("invalid Buffers pod")?,
524
474
  ];
525
- if wants_cursor_meta {
526
- params.push(Pod::from_bytes(&cursor_buf).context("invalid Cursor Meta pod")?);
527
- }
528
475
 
529
476
  stream
530
477
  .connect(
@@ -718,7 +665,7 @@ mod tests {
718
665
  origin_x: 0,
719
666
  origin_y: 0,
720
667
  },
721
- cursor_positions: Arc::new(AtomicBool::new(false)),
668
+ cursor: None,
722
669
  geometry: Arc::new(Mutex::new(None)),
723
670
  frames: Arc::new(AtomicU64::new(0)),
724
671
  dropped: Arc::new(AtomicU64::new(0)),
@@ -739,33 +686,3 @@ mod tests {
739
686
  .expect("capture stopped and joined");
740
687
  }
741
688
  }
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
- }
@@ -6,51 +6,28 @@ pub type CursorSink = Box<dyn Fn(CursorPosition) + Send + 'static>;
6
6
 
7
7
  pub struct Reporter {
8
8
  sink: CursorSink,
9
- last: Option<(i32, i32, bool)>,
10
- bitmap_visible: bool,
9
+ last: Option<(i32, i32, bool, Option<(i32, i32)>)>,
10
+ /// The cursor image's hotspot offset, where the source reports one.
11
+ pub hotspot: Option<(i32, i32)>,
11
12
  }
12
13
  impl Reporter {
13
14
  pub fn new(sink: CursorSink) -> Self {
14
15
  Self {
15
16
  sink,
16
17
  last: None,
17
- bitmap_visible: true,
18
+ hotspot: None,
18
19
  }
19
20
  }
20
- pub fn update_spa(
21
- &mut self,
22
- meta: &pipewire::spa::sys::spa_meta_cursor,
23
- bitmap: Option<&pipewire::spa::sys::spa_meta_bitmap>,
24
- width: usize,
25
- height: usize,
26
- ) {
27
- // SPA id zero is no new data, not a visibility change.
28
- if meta.id == 0 {
29
- return;
30
- }
31
- if let Some(bitmap) = bitmap.filter(|b| b.format != 0) {
32
- self.bitmap_visible = bitmap.offset != 0;
33
- }
34
- let (x, y) = (meta.position.x, meta.position.y);
35
- self.update(
36
- x,
37
- y,
38
- self.bitmap_visible
39
- && x >= 0
40
- && y >= 0
41
- && (x as usize) < width
42
- && (y as usize) < height,
43
- );
44
- }
45
21
  pub fn update(&mut self, x: i32, y: i32, visible: bool) {
46
- if self.last == Some((x, y, visible)) {
22
+ if self.last == Some((x, y, visible, self.hotspot)) {
47
23
  return;
48
24
  }
49
- self.last = Some((x, y, visible));
25
+ self.last = Some((x, y, visible, self.hotspot));
50
26
  (self.sink)(CursorPosition {
51
27
  x,
52
28
  y,
53
29
  visible,
30
+ hotspot: self.hotspot,
54
31
  timestamp_us: timestamp_us(Instant::now()),
55
32
  });
56
33
  }
@@ -59,44 +36,6 @@ impl Reporter {
59
36
  #[cfg(test)]
60
37
  mod tests {
61
38
  use super::*;
62
- #[test]
63
- fn spa_no_update_off_source_and_invisible_bitmap_are_distinct() {
64
- use pipewire::spa::sys::{spa_meta_bitmap, spa_meta_cursor, spa_point, spa_rectangle};
65
- let (tx, rx) = std::sync::mpsc::channel();
66
- let mut reporter = Reporter::new(Box::new(move |p| {
67
- tx.send(p).unwrap();
68
- }));
69
- let mut meta = spa_meta_cursor {
70
- id: 1,
71
- flags: 0,
72
- position: spa_point { x: 40, y: 60 },
73
- hotspot: spa_point { x: 2, y: 3 },
74
- bitmap_offset: 0,
75
- };
76
- reporter.update_spa(&meta, None, 800, 600);
77
- meta.id = 0;
78
- reporter.update_spa(&meta, None, 800, 600);
79
- assert_eq!(rx.try_iter().count(), 1);
80
- meta.id = 1;
81
- meta.position.x = -1;
82
- reporter.update_spa(&meta, None, 800, 600);
83
- assert!(!rx.try_recv().unwrap().visible);
84
- meta.position.x = 40;
85
- let bitmap = spa_meta_bitmap {
86
- format: 1,
87
- size: spa_rectangle {
88
- width: 16,
89
- height: 16,
90
- },
91
- stride: 64,
92
- offset: 0,
93
- };
94
- reporter.update_spa(&meta, Some(&bitmap), 800, 600);
95
- assert!(!rx.try_recv().unwrap().visible);
96
- reporter.update_spa(&meta, None, 800, 600);
97
- assert!(rx.try_recv().is_err(), "no new bitmap retains invisibility");
98
- }
99
-
100
39
  #[test]
101
40
  fn reports_initial_motion_and_visibility_but_not_unchanged_polls() {
102
41
  let (tx, rx) = std::sync::mpsc::channel();
@@ -93,6 +93,8 @@ mod session;
93
93
  #[cfg(target_os = "windows")]
94
94
  mod win;
95
95
  #[cfg(target_os = "linux")]
96
+ mod wl_cursor;
97
+ #[cfg(target_os = "linux")]
96
98
  mod x11;
97
99
  #[cfg(any(target_os = "macos", target_os = "windows"))]
98
100
  #[path = "x11_mac.rs"]
@@ -391,7 +393,6 @@ async fn probe_portal(seconds: u64) -> Result<()> {
391
393
  width,
392
394
  height,
393
395
  30,
394
- None,
395
396
  Box::new(move |frame, seq, _raw| {
396
397
  let _ = tx.send((frame.width, frame.height, seq));
397
398
  }),
@@ -558,10 +559,13 @@ fn render_event(notice: session::Notice) -> Option<String> {
558
559
  "session.state",
559
560
  serde_json::json!({ "sessionId": session_id, "capture": capture, "transport": transport, "firstFrame": first_frame }),
560
561
  ),
561
- session::SessionEvent::Cursor { position } => (
562
- "session.cursor",
563
- serde_json::json!({ "sessionId": session_id, "x": position.x, "y": position.y, "visible": position.visible, "timestamp_us": position.timestamp_us }),
564
- ),
562
+ session::SessionEvent::Cursor { position } => ("session.cursor", {
563
+ let mut params = serde_json::json!({ "sessionId": session_id, "x": position.x, "y": position.y, "visible": position.visible, "timestamp_us": position.timestamp_us });
564
+ if let Some((x, y)) = position.hotspot {
565
+ params["hotspot"] = serde_json::json!({ "x": x, "y": y });
566
+ }
567
+ params
568
+ }),
565
569
  session::SessionEvent::Frame {
566
570
  seq,
567
571
  damage,
@@ -892,6 +896,7 @@ mod tests {
892
896
  x: 32,
893
897
  y: 64,
894
898
  visible: false,
899
+ hotspot: None,
895
900
  timestamp_us: 123456,
896
901
  },
897
902
  },
@@ -2,8 +2,8 @@
2
2
  //!
3
3
  //! The portal is the only capture path this engine asks the compositor for: it
4
4
  //! is what carries the user's consent, works across Wayland compositors, and
5
- //! hands back a PipeWire remote that [`crate::capture`] consumes. No
6
- //! compositor-private protocol is used.
5
+ //! hands back a PipeWire remote that [`crate::capture`] consumes. Hidden-cursor
6
+ //! positions come from the compositor itself, through [`crate::wl_cursor`].
7
7
 
8
8
  use anyhow::{Context, Result};
9
9
  use ashpd::desktop::screencast::{CursorMode, Screencast, SourceType};
@@ -134,24 +134,29 @@ pub async fn open(
134
134
  })
135
135
  }
136
136
 
137
+ /// Either mode keeps cursor pixels out of the stream; positions never come
138
+ /// from the portal.
137
139
  fn choose_cursor_mode(available: BitFlags<CursorMode>) -> Result<CursorMode> {
138
- if available.contains(CursorMode::Metadata) {
139
- return Ok(CursorMode::Metadata);
140
- }
141
- anyhow::bail!("cursor_positions_unavailable: portal does not provide cursor metadata")
140
+ [CursorMode::Hidden, CursorMode::Metadata]
141
+ .into_iter()
142
+ .find(|mode| available.contains(*mode))
143
+ .context("cursor-unavailable: portal cannot leave the cursor out of capture")
142
144
  }
143
145
 
144
146
  #[cfg(test)]
145
147
  mod tests {
146
148
  use super::*;
147
149
  #[test]
148
- fn hidden_requires_metadata_and_refuses_hidden_only_portals() {
150
+ fn hidden_takes_any_cursor_free_mode_and_refuses_embedded_only_portals() {
149
151
  assert_eq!(
150
152
  choose_cursor_mode(CursorMode::Metadata | CursorMode::Hidden).unwrap(),
153
+ CursorMode::Hidden
154
+ );
155
+ assert_eq!(
156
+ choose_cursor_mode(CursorMode::Metadata.into()).unwrap(),
151
157
  CursorMode::Metadata
152
158
  );
153
- let error = choose_cursor_mode(CursorMode::Hidden.into()).unwrap_err();
154
- assert!(error.to_string().contains("cursor_positions_unavailable:"));
155
- assert!(choose_cursor_mode(CursorMode::Embedded.into()).is_err());
159
+ let error = choose_cursor_mode(CursorMode::Embedded.into()).unwrap_err();
160
+ assert!(error.to_string().contains("cursor-unavailable:"));
156
161
  }
157
162
  }
@@ -569,5 +569,6 @@ pub struct CursorPosition {
569
569
  pub x: i32,
570
570
  pub y: i32,
571
571
  pub visible: bool,
572
+ pub hotspot: Option<(i32, i32)>,
572
573
  pub timestamp_us: u64,
573
574
  }