@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 +14 -9
- package/dist/protocol.d.ts +9 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js.map +1 -1
- package/docs/PROTOCOL.md +38 -23
- package/engine/Cargo.lock +2 -1
- package/engine/Cargo.toml +7 -4
- package/engine/src/capture.rs +7 -90
- package/engine/src/cursor.rs +7 -68
- package/engine/src/main.rs +10 -5
- package/engine/src/portal.rs +15 -10
- package/engine/src/protocol.rs +1 -0
- package/engine/src/session.rs +73 -228
- package/engine/src/wl_cursor.rs +367 -0
- package/package.json +3 -3
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
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
|
280
|
-
|
|
281
|
-
`
|
|
282
|
-
`
|
|
283
|
-
|
|
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).
|
package/dist/protocol.d.ts
CHANGED
|
@@ -32,7 +32,7 @@ export interface EngineCapabilities {
|
|
|
32
32
|
cursor_modes?: Array<'embedded' | 'hidden'>;
|
|
33
33
|
cursor_positions?: {
|
|
34
34
|
x11: boolean;
|
|
35
|
-
|
|
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';
|
package/dist/protocol.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,eAAO,MAAM,gBAAgB,IAAI,CAAC;AAElC,MAAM,WAAW,WAAW;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,kBAAkB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IAChD,sFAAsF;IACtF,GAAG,EAAE;QAAE,SAAS,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAA;KAAE,CAAC;IAC3D,OAAO,EAAE;QACL,SAAS,EAAE,MAAM,CAAC;QAClB,0EAA0E;QAC1E,QAAQ,EAAE,MAAM,EAAE,CAAC;QACnB,OAAO,EAAE,MAAM,EAAE,CAAC;QAClB,MAAM,EAAE,MAAM,CAAC;QACf,YAAY,CAAC,EAAE,KAAK,CAAC,UAAU,GAAG,QAAQ,CAAC,CAAC;QAC5C,gBAAgB,CAAC,EAAE;YAAE,GAAG,EAAE,OAAO,CAAC;YAAC,
|
|
1
|
+
{"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"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"}
|
package/dist/protocol.js.map
CHANGED
|
@@ -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;
|
|
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, "
|
|
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
|
|
454
|
-
frame interval while a newer frame is
|
|
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.
|
|
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
|
|
494
|
-
**
|
|
495
|
-
omitted
|
|
496
|
-
|
|
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}`;
|
|
499
|
-
not just the requested
|
|
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,"
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
or
|
|
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
|
-
|
|
519
|
-
|
|
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
|
|
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.
|
|
525
|
-
|
|
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.
|
|
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.
|
|
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
|
|
44
|
-
wayland-
|
|
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"
|
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::{
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}
|
package/engine/src/cursor.rs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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();
|
package/engine/src/main.rs
CHANGED
|
@@ -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
|
-
"
|
|
563
|
-
|
|
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
|
},
|
package/engine/src/portal.rs
CHANGED
|
@@ -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.
|
|
6
|
-
//! compositor
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
|
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::
|
|
154
|
-
assert!(error.to_string().contains("
|
|
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
|
}
|