@desklink/host 0.5.8 → 0.5.9

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.
@@ -208,6 +208,7 @@ export type EngineEvent = {
208
208
  type: 'offer';
209
209
  sdp: string;
210
210
  };
211
+ geometry?: SurfaceGeometry;
211
212
  };
212
213
  } | {
213
214
  event: 'session.candidate';
@@ -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":"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,OAAO,EAAE,2BAA2B,CAAA;SAAE,CAAC;QAC1E,KAAK,EAAE,OAAO,CAAC;QACf;wEACgE;QAChE,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAC7B,kDAAkD;QAClD,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;QACzB,QAAQ,CAAC,EAAE,KAAK,CAAC;YACb,EAAE,EAAE,MAAM,CAAC;YACX,KAAK,EAAE,MAAM,CAAC;YACd,MAAM,EAAE,MAAM,CAAC;YACf,KAAK,EAAE,MAAM,CAAC;YACd,MAAM,EAAE;gBAAE,KAAK,EAAE,MAAM,CAAC;gBAAC,MAAM,EAAE,MAAM,CAAA;aAAE,CAAC;YAC1C,MAAM,EAAE;gBAAE,CAAC,EAAE,MAAM,CAAC;gBAAC,CAAC,EAAE,MAAM,CAAA;aAAE,CAAC;YACjC,IAAI,EAAE,OAAO,CAAC;SACjB,CAAC,CAAC;QACH,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,kBAAkB,CAAC,EAAE;YAAE,MAAM,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAA;SAAE,GAAG,IAAI,CAAC;KAClE,CAAC;IACF,MAAM,EAAE;QAAE,MAAM,EAAE,MAAM,EAAE,CAAC;QAAC,QAAQ,EAAE,OAAO,CAAA;KAAE,CAAC;IAChD,KAAK,EAAE;QACH,SAAS,EAAE,MAAM,CAAC;QAClB,OAAO,EAAE,OAAO,CAAC;QACjB,KAAK,EAAE,OAAO,CAAC;QACf,QAAQ,EAAE,OAAO,CAAC;QAClB,IAAI,EAAE,MAAM,EAAE,CAAC;QACf,MAAM,EAAE,MAAM,CAAC;QACf,kBAAkB,EAAE;YAAE,MAAM,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAA;SAAE,GAAG,IAAI,CAAC;QAC9D,KAAK,EAAE,MAAM,CAAC;KACjB,CAAC;IACF,SAAS,EAAE;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE,MAAM,EAAE,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;CAClF;AAED,MAAM,WAAW,eAAe;IAC5B,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1C,OAAO,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3C,MAAM,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACpC;AAED;;;GAGG;AACH,MAAM,MAAM,aAAa,GACnB;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,GAClB;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GACxC;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAExC,MAAM,WAAW,UAAU;IACvB,IAAI,EAAE,UAAU,GAAG,QAAQ,GAAG,UAAU,GAAG,aAAa,CAAC;IACzD,SAAS,EAAE,OAAO,CAAC;IACnB,0DAA0D;IAC1D,MAAM,CAAC,EAAE,2BAA2B,CAAC;IACrC,0DAA0D;IAC1D,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,SAAS;IACtB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,MAAM,EAAE,OAAO,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,kBAAkB;IAC/B,6EAA6E;IAC7E,MAAM,CAAC,EAAE,UAAU,GAAG,QAAQ,CAAC;IAC/B,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,SAAS,GAAG,WAAW,CAAC,CAAC;IACtD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yGAAyG;IACzG,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,EAAE,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,yFAAyF;IACzF,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,+EAA+E;IAC/E,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,0EAA0E;IAC1E,KAAK,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC1B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,2DAA2D;IAC3D,KAAK,CAAC,EAAE,SAAS,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE;QACJ,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;QACf,MAAM,EAAE;YAAE,CAAC,EAAE,MAAM,CAAC;YAAC,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;KACpC,CAAC;IACF,QAAQ,EAAE,eAAe,CAAC;CAC7B;AAED,MAAM,MAAM,WAAW,GACjB;IAAE,KAAK,EAAE,gBAAgB,CAAC;IAAC,MAAM,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE;YAAE,CAAC,EAAE,MAAM,CAAC;YAAC,CAAC,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,CAAA;CAAE,GAC5J;IAAE,KAAK,EAAE,qBAAqB,CAAC;IAAC,MAAM,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE;YAAE,IAAI,EAAE,OAAO,CAAC;YAAC,GAAG,EAAE,MAAM,CAAA;SAAE,CAAC;QAAC,QAAQ,CAAC,EAAE,eAAe,CAAA;KAAE,CAAA;CAAE,GAC5J;IACI,KAAK,EAAE,mBAAmB,CAAC;IAC3B,MAAM,EAAE;QACJ,SAAS,EAAE,MAAM,CAAC;QAClB,UAAU,EAAE,MAAM,CAAC;QACnB,SAAS,EAAE,MAAM,CAAC;QAClB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;QACtB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;KAChC,CAAC;CACL,GACD;IAAE,KAAK,EAAE,uBAAuB,CAAC;IAAC,MAAM,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,EAAE,EAAE,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,GACzH;IAAE,KAAK,EAAE,uBAAuB,CAAC;IAAC,MAAM,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,GACnI;IAAE,KAAK,EAAE,eAAe,CAAC;IAAC,MAAM,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,OAAO,CAAA;KAAE,CAAA;CAAE,GAClH;IAAE,KAAK,EAAE,sBAAsB,CAAC;IAAC,MAAM,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,GAC/E;IAAE,KAAK,EAAE,yBAAyB,CAAC;IAAC,MAAM,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,GACnF;IAAE,KAAK,EAAE,iBAAiB,CAAC;IAAC,MAAM,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,WAAW,GAAG,OAAO,GAAG,OAAO,CAAA;KAAE,CAAA;CAAE,CAAC;AAEzH,MAAM,MAAM,eAAe,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC;AAEnD,MAAM,WAAW,cAAc;IAC3B,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,4EAA4E;IAC5E,aAAa,EAAE,MAAM,CAAC;IACtB,wFAAwF;IACxF,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,4EAA4E;IAC5E,cAAc,EAAE,MAAM,CAAC;IACvB,qEAAqE;IACrE,cAAc,EAAE,MAAM,CAAC;IACvB,4EAA4E;IAC5E,YAAY,EAAE,MAAM,CAAC;IACrB,kEAAkE;IAClE,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,gBAAgB,EAAE,MAAM,CAAC;CAC5B;AAED,yEAAyE;AACzE,qBAAa,aAAc,SAAQ,KAAK;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAEV,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;CAK5C;AAED,0DAA0D;AAC1D,MAAM,MAAM,YAAY,GAAG;IACvB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,KAAK,CAAC;CACjB,GAAG;IAAE,KAAK,EAAE,IAAI,CAAA;CAAE,CAAC"}
package/docs/PROTOCOL.md CHANGED
@@ -265,6 +265,20 @@ rate and X11 capture loop; portal capture offers it as the preferred PipeWire
265
265
  frame rate, and macOS sets its ScreenCaptureKit stream minimum frame interval.
266
266
  A source may still supply fewer frames.
267
267
 
268
+ `ice_servers` carries the STUN/TURN servers the user brought, one object per
269
+ server with every URL that server lists. Each URL an entry lists reaches the
270
+ WebRTC stack with that entry's `username` and `credential` — listing
271
+ `stun:a` and `stun:b` in one entry uses both, so a fallback behind an
272
+ unreachable primary still gathers candidates. A TURN URL (`turn:`/`turns:`)
273
+ whose transport is not UDP — `?transport=tcp`, or a bare `turns:` URL, which
274
+ is TLS over TCP by default — is one the WebRTC stack cannot dial: the engine
275
+ leaves it out of the configuration and reports it once with a
276
+ `session.iceServerSkipped` warning event naming the URL, so a skipped URL is
277
+ never a silent drop. Everything else in the entry — including a URL the
278
+ parser cannot read — is handed to the WebRTC stack unchanged, whose own
279
+ verdict then applies: for example a `turn:` URL on an entry without both a
280
+ `username` and a `credential` refuses the open in the stack's own words.
281
+
268
282
  `agent_indicator` is opt-in local feedback for an agent controller; desklink-axi
269
283
  sets it for sessions it drives (except Windows); human phone control omits it. On
270
284
  Linux it adds an eased arrow inside a halo that brightens on clicks and keys,
@@ -275,9 +289,11 @@ masked before capture/diff (pixels behind the effect can remain briefly stale).
275
289
  On a located Hyprland window share each pointer cue goes to the desktop point
276
290
  the pointer landed on — the window's place plus the tapped point (see Input and
277
291
  clipboard) — and the window's picture holds no layer surface, so nothing is
278
- masked; a refused tap draws no cue. Known limit: the cue's surface sits on the
279
- one output the compositor gave it as the share started, and a tap landing on
280
- another output draws no cue. On bare X11 it is an override-redirect, click-through window that follows the
292
+ masked; a refused tap draws no cue. When a tap lands on another output than
293
+ the cue helper is on, the engine moves the helper there — respawning it with
294
+ the landed layout point, which selects that output exactly like a point
295
+ marker — and the cue draws at the landed point with its edge glow; a tap on
296
+ the helper's own output reuses it with no new process. On bare X11 it is an override-redirect, click-through window that follows the
281
297
  pointer, so it stays
282
298
  visible over windows that set their own cursor and never replaces the cursor.
283
299
  Its size follows `Xft.dpi` from 96–384 DPI, defaulting to 1x otherwise;
@@ -316,6 +332,17 @@ the SDP and candidates to the client and brings back the answer.
316
332
  "description":{"type":"offer","sdp":"v=0\r\n…"}}}
317
333
  ```
318
334
 
335
+ A renegotiation offer may include `geometry` when the captured source changes
336
+ size. It uses the same shape as the `session.open` result; consumers should
337
+ apply its encoded dimensions to the presented surface. Older hosts omit it.
338
+
339
+ ```jsonc
340
+ {"event":"session.description","params":{"sessionId":"…","generation":1,
341
+ "description":{"type":"offer","sdp":"v=0\r\n…"},
342
+ "geometry":{"source":{"width":1200,"height":600},
343
+ "encoded":{"width":1200,"height":600},"origin":{"x":0,"y":0}}}}
344
+ ```
345
+
319
346
  ```jsonc
320
347
  {"id":5,"method":"session.description","params":{"session_id":"…","generation":1,
321
348
  "description":{"type":"answer","sdp":"v=0\r\n…"}}}
@@ -500,7 +527,9 @@ FIR), and when the coded size changes that way, so a receiver never has to scale
500
527
  references across a size change. `target_kbps` is the current rate target after
501
528
  any back-off: for a standing queue on the path, measured from the receiver's
502
529
  transport-wide congestion feedback (RTP `transport-cc`), and for the loss
503
- its receiver reports carry. It starts at the requested ceiling capped at
530
+ its receiver reports carry. A path that runs over ICE-TCP backs off only on a
531
+ queue above 100 ms, not 25 ms, so a relay's delivery jitter does not pull the
532
+ target to its floor. It starts at the requested ceiling capped at
504
533
  3000 kbps on an unknown link, then grows toward the ceiling on clean
505
534
  loss/delay feedback. `codec` is what frames go out in
506
535
  (`vp9` or `h264`) and `encoder` what codes them (`libvpx`, `videotoolbox`,
@@ -611,7 +640,9 @@ origin, before video scaling (`geometry.source`, not `geometry.encoded`). On X11
611
640
  this is the root screen; on macOS it is the selected display, with native pixels
612
641
  converted to display points. On a located Hyprland window share (see Input and
613
642
  clipboard) they are the window's own pixels, and the ring shows on the desktop
614
- point a press there would land, on the output holding it; a point the window
643
+ point a press there would land, on the output holding it; when a later point
644
+ lands on another output the helper moves there, and a point on its own output
645
+ reuses it with no new process. A point the window
615
646
  cannot take there is refused with `indicator-unavailable` and the reason. Both
616
647
  coordinates must be within its bounds. The
617
648
  ring may clip at an edge; its label stays inside the display. `timeout_ms`
package/engine/Cargo.lock CHANGED
@@ -536,7 +536,7 @@ checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
536
536
 
537
537
  [[package]]
538
538
  name = "desklink-host"
539
- version = "0.5.8"
539
+ version = "0.5.9"
540
540
  dependencies = [
541
541
  "anyhow",
542
542
  "ashpd",
@@ -557,6 +557,7 @@ dependencies = [
557
557
  "pipewire",
558
558
  "rand 0.8.8",
559
559
  "rtc",
560
+ "rtc-ice",
560
561
  "serde",
561
562
  "serde_json",
562
563
  "sha2",
package/engine/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "desklink-host"
3
- version = "0.5.8"
3
+ version = "0.5.9"
4
4
  edition = "2021"
5
5
  rust-version = "1.97.1"
6
6
  license = "Apache-2.0"
@@ -18,7 +18,7 @@ base64 = "0.22"
18
18
 
19
19
  [target.'cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))'.dependencies]
20
20
  anyhow = "1"
21
- tokio = { version = "1", features = ["rt-multi-thread", "macros", "io-util", "io-std", "sync", "time", "signal"] }
21
+ tokio = { version = "1", features = ["rt-multi-thread", "macros", "io-util", "io-std", "sync", "time", "signal", "net"] }
22
22
  libc = "0.2"
23
23
  bytes = "1"
24
24
  # rtc-sctp 0.21.1 is required through this stack: 0.21.0 replaces an
@@ -27,6 +27,9 @@ bytes = "1"
27
27
  # session. Cargo.lock pins 0.21.1; drop the pin when a later release carries it.
28
28
  webrtc = { version = "0.21", default-features = false, features = ["runtime-tokio", "crypto-ring"] }
29
29
  rtc = { version = "0.21", default-features = false, features = ["crypto-ring"] }
30
+ # Parses ice server URLs when the engine sorts out which ones the WebRTC
31
+ # stack can dial; the [patch.crates-io] entry below keeps this the vendored copy.
32
+ rtc-ice = { version = "0.21", default-features = false }
30
33
  log = "0.4"
31
34
  async-trait = "0.1"
32
35
  rand = "0.8"
@@ -252,7 +252,9 @@ static int overlay_main(int source_w,int source_h,int point_mode,int origin_know
252
252
  }
253
253
  if(wl_display_roundtrip(o.display)<0)return 2;
254
254
  }
255
- if(point_mode) {
255
+ // Point markers always name their output; a desktop agent cue does once
256
+ // the engine has seen a press land, so the cue opens on that output.
257
+ if(point_mode || (desktop && origin_known)) {
256
258
  for(int i=0;i<o.output_count;i++) {
257
259
  struct Output *out=&o.outputs[i];
258
260
  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;o.mine=out;matches++;}
@@ -269,8 +271,9 @@ static int overlay_main(int source_w,int source_h,int point_mode,int origin_know
269
271
  FT_Library library=NULL;FT_Face font=point_mode?point_font(&library):NULL;
270
272
  if(point_mode&&!font){wl_display_disconnect(o.display);return 2;}
271
273
  o.surface=wl_compositor_create_surface(o.compositor);
272
- // An agent cue's surface lands where the compositor puts it; it learns which output that is.
273
- if(desktop && !point_mode)wl_surface_add_listener(o.surface,&surface_listener,&o);
274
+ // A desktop surface learns which output it is on, whether the compositor
275
+ // placed it or a layout origin selected it.
276
+ if(desktop)wl_surface_add_listener(o.surface,&surface_listener,&o);
274
277
  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");
275
278
  wl_proxy_add_listener(o.layer,layer_listener,&o);
276
279
  uint32_t anchor,keyboard_focus,empty_input;
@@ -356,7 +359,7 @@ static int overlay_main(int source_w,int source_h,int point_mode,int origin_know
356
359
  return 0;
357
360
  }
358
361
 
359
- int desklink_agent_overlay_wayland(int w,int h,int desktop){return overlay_main(w,h,0,0,0,0,desktop);}
362
+ int desklink_agent_overlay_wayland(int w,int h,int desktop,int known,int x,int y){return overlay_main(w,h,0,known,x,y,desktop);}
360
363
  int desklink_point_overlay_wayland(int w,int h,int known,int x,int y,int desktop){return overlay_main(w,h,1,known,x,y,desktop);}
361
364
 
362
365
  // Read-only capability probe for idempotent clear before a helper exists.
@@ -303,6 +303,31 @@ impl WindowLocator {
303
303
  }
304
304
  }
305
305
 
306
+ /// The name of the monitor holding desktop point `(px, py)`, if any.
307
+ /// Geometry math matches `WindowLocator::locate`'s on-screen check, so a
308
+ /// point a press landed on maps to the output whose overlay must draw it.
309
+ pub fn monitor_containing(px: f64, py: f64) -> Result<Option<String>, String> {
310
+ let monitors = query("j/monitors")?;
311
+ for monitor in monitors.as_array().into_iter().flatten() {
312
+ if monitor["disabled"].as_bool() == Some(true) {
313
+ continue;
314
+ }
315
+ let scale = monitor["scale"].as_f64().filter(|scale| *scale > 0.0);
316
+ if let (Some(mx), Some(my), Some(mw), Some(mh), Some(scale)) = (
317
+ monitor["x"].as_f64(),
318
+ monitor["y"].as_f64(),
319
+ monitor["width"].as_f64(),
320
+ monitor["height"].as_f64(),
321
+ scale,
322
+ ) {
323
+ if (mx..mx + mw / scale).contains(&px) && (my..my + mh / scale).contains(&py) {
324
+ return Ok(monitor["name"].as_str().map(str::to_owned));
325
+ }
326
+ }
327
+ }
328
+ Ok(None)
329
+ }
330
+
306
331
  /// A window's box in the layout: left, top, width, height.
307
332
  fn bounds(window: &Value) -> Option<(f64, f64, f64, f64)> {
308
333
  let pair = |key: &str| Some((window[key][0].as_f64()?, window[key][1].as_f64()?));
@@ -222,19 +222,29 @@ impl Indicator {
222
222
  }
223
223
 
224
224
  /// `desktop`: cues are compositor layout coordinates, drawn only where
225
- /// they fall on the output the overlay opened on.
225
+ /// they fall on the output the overlay opened on. `origin` selects that
226
+ /// output by layout point, reusing the point helper's xdg-output
227
+ /// selection; without one the compositor places the surface.
226
228
  #[cfg(target_os = "linux")]
227
- pub fn start_wayland(width: usize, height: usize, desktop: bool) -> Result<Self> {
229
+ pub fn start_wayland(
230
+ width: usize,
231
+ height: usize,
232
+ desktop: bool,
233
+ origin: Option<(i32, i32)>,
234
+ ) -> Result<Self> {
228
235
  let executable = std::env::current_exe()?;
229
- Self::spawn(
230
- Command::new(executable)
231
- .arg("agent-overlay-wayland")
232
- .arg(width.to_string())
233
- .arg(height.to_string())
234
- .args(desktop.then_some("desktop")),
235
- 1.0,
236
- 1.0,
237
- )
236
+ let mut command = Command::new(executable);
237
+ command
238
+ .arg("agent-overlay-wayland")
239
+ .arg(width.to_string())
240
+ .arg(height.to_string());
241
+ if desktop {
242
+ command.arg("desktop");
243
+ if let Some((x, y)) = origin {
244
+ command.arg(x.to_string()).arg(y.to_string());
245
+ }
246
+ }
247
+ Self::spawn(&mut command, 1.0, 1.0)
238
248
  }
239
249
 
240
250
  #[cfg(target_os = "linux")]
@@ -369,7 +379,14 @@ mod tests {
369
379
  };
370
380
  let mut drained = vec![];
371
381
  for _ in 0..200 {
372
- drained.extend(feed.drain().into_iter().map(|cue| cue.x));
382
+ // Each drain re-reports the last two remembered states, so the
383
+ // same state can arrive in several drains when the helper's lines
384
+ // are split across reads. Keep each state once, in pipe order.
385
+ for cue in feed.drain() {
386
+ if !drained.contains(&cue.x) {
387
+ drained.push(cue.x);
388
+ }
389
+ }
373
390
  if drained.len() >= 3 {
374
391
  break;
375
392
  }
@@ -136,7 +136,14 @@ use tokio::sync::mpsc as tokio_mpsc;
136
136
  unsafe extern "C" {
137
137
  fn desklink_point_overlay_main(display: *const std::ffi::c_char) -> i32;
138
138
  fn desklink_agent_overlay_main(display: *const std::ffi::c_char) -> i32;
139
- fn desklink_agent_overlay_wayland(source_w: i32, source_h: i32, desktop: i32) -> i32;
139
+ fn desklink_agent_overlay_wayland(
140
+ source_w: i32,
141
+ source_h: i32,
142
+ desktop: i32,
143
+ origin_known: i32,
144
+ origin_x: i32,
145
+ origin_y: i32,
146
+ ) -> i32;
140
147
  fn desklink_wayland_layer_shell_available() -> i32;
141
148
  fn desklink_point_overlay_wayland(
142
149
  w: i32,
@@ -212,7 +219,21 @@ fn main() {
212
219
  2
213
220
  } else {
214
221
  let desktop = i32::from(args.get(3).map(String::as_str) == Some("desktop"));
215
- unsafe { desklink_agent_overlay_wayland(width, height, desktop) }
222
+ // A desktop cue respawned after a press names the layout
223
+ // point that chose its output: `w h desktop ox oy`.
224
+ let (known, ox, oy) = match (args.get(4), args.get(5)) {
225
+ (Some(x), Some(y)) => match (x.parse(), y.parse()) {
226
+ (Ok(x), Ok(y)) => (1, x, y),
227
+ _ => (0, 0, 0),
228
+ },
229
+ _ => (0, 0, 0),
230
+ };
231
+ let (known, ox, oy) = if desktop == 1 {
232
+ (known, ox, oy)
233
+ } else {
234
+ (0, 0, 0)
235
+ };
236
+ unsafe { desklink_agent_overlay_wayland(width, height, desktop, known, ox, oy) }
216
237
  }
217
238
  }
218
239
  "capabilities" => {
@@ -542,6 +563,47 @@ async fn serve() -> Result<()> {
542
563
  Ok(())
543
564
  }
544
565
 
566
+ /// Splits the requested ICE servers into the configuration the peer gets and
567
+ /// the URLs this engine leaves out. Every URL the WebRTC stack can dial goes
568
+ /// through with its entry's username and credential — all of an entry's URLs,
569
+ /// so a fallback behind an unreachable primary still gathers candidates. A
570
+ /// TURN URL whose transport is not UDP is one the stack cannot dial, so it is
571
+ /// skipped here and reported once by the caller, never dropped silently. A
572
+ /// URL the parser cannot read is forwarded unchanged: the peer connection
573
+ /// owns that verdict and refuses the open in its own words.
574
+ #[cfg(any(
575
+ target_os = "linux",
576
+ all(any(target_os = "macos", target_os = "windows"), desklink_vpx)
577
+ ))]
578
+ fn usable_ice_servers(
579
+ servers: Vec<protocol::IceServerParam>,
580
+ ) -> (
581
+ Vec<(Vec<String>, Option<String>, Option<String>)>,
582
+ Vec<String>,
583
+ ) {
584
+ use rtc_ice::url::{ProtoType, SchemeType, Url};
585
+ let mut usable = Vec::new();
586
+ let mut skipped = Vec::new();
587
+ for server in servers {
588
+ let mut urls = Vec::with_capacity(server.urls.len());
589
+ for url in server.urls {
590
+ let undialable = Url::parse_url(&url).is_ok_and(|parsed| {
591
+ matches!(parsed.scheme, SchemeType::Turn | SchemeType::Turns)
592
+ && parsed.proto != ProtoType::Udp
593
+ });
594
+ if undialable {
595
+ skipped.push(url);
596
+ } else {
597
+ urls.push(url);
598
+ }
599
+ }
600
+ if !urls.is_empty() {
601
+ usable.push((urls, server.username, server.credential));
602
+ }
603
+ }
604
+ (usable, skipped)
605
+ }
606
+
545
607
  #[cfg(any(
546
608
  target_os = "linux",
547
609
  all(any(target_os = "macos", target_os = "windows"), desklink_vpx)
@@ -616,6 +678,10 @@ fn render_event(notice: session::Notice) -> Option<String> {
616
678
  "session.capture.stopped",
617
679
  serde_json::json!({ "sessionId": session_id, "reason": reason }),
618
680
  ),
681
+ session::SessionEvent::IceServerSkipped { url } => (
682
+ "session.iceServerSkipped",
683
+ serde_json::json!({ "sessionId": session_id, "url": url }),
684
+ ),
619
685
  session::SessionEvent::Revoked { reason, code } => (
620
686
  "session.revoked",
621
687
  serde_json::json!({ "sessionId": session_id, "reason": reason, "code": code }),
@@ -672,6 +738,7 @@ async fn dispatch(
672
738
  }
673
739
  let params: protocol::OpenParams = serde_json::from_value(request.params.clone())
674
740
  .map_err(|error| ErrorBody::new("malformed", error.to_string()))?;
741
+ let usable = usable_ice_servers(params.ice_servers);
675
742
  let open = session::OpenRequest {
676
743
  source: params.source,
677
744
  cursor: params.cursor,
@@ -685,16 +752,7 @@ async fn dispatch(
685
752
  agent_indicator: params.agent_indicator,
686
753
  local_frames: params.local_frames,
687
754
  audio: params.audio,
688
- ice_servers: params
689
- .ice_servers
690
- .into_iter()
691
- .filter_map(|server| {
692
- server
693
- .urls
694
- .first()
695
- .map(|url| (url.clone(), server.username, server.credential))
696
- })
697
- .collect(),
755
+ ice_servers: usable.0,
698
756
  };
699
757
  let session = session::Session::open(open, events.clone())
700
758
  .await
@@ -713,6 +771,15 @@ async fn dispatch(
713
771
  "agentIndicator": session.agent_indicator(),
714
772
  "audio": session.audio_info(),
715
773
  });
774
+ // URLs the WebRTC stack cannot dial were left out of the peer's
775
+ // configuration; each skipped one is named once instead of
776
+ // vanishing from a multi-URL entry without a trace.
777
+ for url in usable.1 {
778
+ let _ = events.send(session::Notice {
779
+ session_id: session.id().to_owned(),
780
+ event: session::SessionEvent::IceServerSkipped { url },
781
+ });
782
+ }
716
783
  *current = Some(session);
717
784
  Ok(result)
718
785
  }
@@ -947,6 +1014,26 @@ mod tests {
947
1014
  );
948
1015
  }
949
1016
 
1017
+ #[test]
1018
+ fn the_skipped_ice_server_warning_names_the_url_once() {
1019
+ let line = render_event(session::Notice {
1020
+ session_id: String::from("session-1"),
1021
+ event: session::SessionEvent::IceServerSkipped {
1022
+ url: String::from("turn:turn.example.com:3478?transport=tcp"),
1023
+ },
1024
+ })
1025
+ .expect("a skipped-URL warning is renderable");
1026
+ let parsed: serde_json::Value = serde_json::from_str(&line).unwrap();
1027
+ assert_eq!(parsed["event"], "session.iceServerSkipped");
1028
+ assert_eq!(
1029
+ parsed["params"],
1030
+ serde_json::json!({
1031
+ "sessionId": "session-1",
1032
+ "url": "turn:turn.example.com:3478?transport=tcp"
1033
+ })
1034
+ );
1035
+ }
1036
+
950
1037
  #[tokio::test]
951
1038
  async fn the_state_notification_carries_exactly_what_the_document_promises() {
952
1039
  let line = render_event(session::Notice {
@@ -23,7 +23,7 @@ use rtc::peer_connection::configuration::media_engine::{
23
23
  use rtc::peer_connection::configuration::RTCConfigurationBuilder;
24
24
  use rtc::peer_connection::event::RTCPeerConnectionIceEvent;
25
25
  use rtc::peer_connection::sdp::RTCSessionDescription;
26
- use rtc::peer_connection::transport::{RTCIceCandidateInit, RTCIceServer};
26
+ use rtc::peer_connection::transport::{RTCIceCandidateInit, RTCIceProtocol, RTCIceServer};
27
27
  use rtc::rtcp::payload_feedbacks::full_intra_request::FullIntraRequest;
28
28
  use rtc::rtcp::payload_feedbacks::picture_loss_indication::PictureLossIndication;
29
29
  use rtc::rtcp::receiver_report::ReceiverReport;
@@ -47,7 +47,7 @@ use webrtc::peer_connection::{
47
47
  };
48
48
  use webrtc::runtime::default_runtime;
49
49
 
50
- // desklink scout trace (dl-roam-postnom-attr): engine-side restart timeline.
50
+ // desklink scout trace: engine-side restart timeline.
51
51
  // Enabled with `DESKLINK_ICE_TRACE=1`; stamps microseconds since the Unix epoch
52
52
  // so these lines interleave with the vendored ICE agent's `[ice ...]` lines in
53
53
  // one order. Droppable: grep `desklink scout trace`.
@@ -258,7 +258,10 @@ pub enum PeerEvent {
258
258
  /// Transport settings the consumer chose for this session.
259
259
  #[derive(Debug, Clone)]
260
260
  pub struct TransportOptions {
261
- pub ice_servers: Vec<(String, Option<String>, Option<String>)>,
261
+ /// One entry per ICE server the consumer asked for, carrying every URL
262
+ /// that entry listed; the engine has already left out the ones the
263
+ /// WebRTC stack cannot dial and reported those to the consumer.
264
+ pub ice_servers: Vec<(Vec<String>, Option<String>, Option<String>)>,
262
265
  /// Listen for ICE over TCP on the loopback too. A phone that reaches this
263
266
  /// computer only through SSH has no UDP path; it forwards this port over
264
267
  /// the SSH connection it already has, and the picture rides that.
@@ -474,7 +477,7 @@ impl PeerConnectionEventHandler for Handler {
474
477
  }
475
478
 
476
479
  async fn on_connection_state_change(&self, state: RTCPeerConnectionState) {
477
- // desklink scout trace (dl-roam-postnom-attr): the engine's rtc-level
480
+ // desklink scout trace: the engine's rtc-level
478
481
  // transport state event, on the same microsecond clock as the vendored
479
482
  // agent's `[ice ...]` lines, so the rtc Connected gate is separable.
480
483
  if dl_trace_on() {
@@ -898,8 +901,8 @@ impl VideoPeer {
898
901
  options
899
902
  .ice_servers
900
903
  .iter()
901
- .map(|(url, username, credential)| RTCIceServer {
902
- urls: vec![url.clone()],
904
+ .map(|(urls, username, credential)| RTCIceServer {
905
+ urls: urls.clone(),
903
906
  username: username.clone().unwrap_or_default(),
904
907
  credential: credential.clone().unwrap_or_default(),
905
908
  })
@@ -1130,6 +1133,21 @@ impl VideoPeer {
1130
1133
  self.connected.load(Ordering::SeqCst)
1131
1134
  }
1132
1135
 
1136
+ /// Whether ICE selected a TCP candidate pair (the relay's ICE-TCP path).
1137
+ /// False until a pair is selected.
1138
+ pub async fn selected_pair_is_tcp(&self) -> bool {
1139
+ let Some(sender) = self.peer.get_senders().await.into_iter().next() else {
1140
+ return false;
1141
+ };
1142
+ let Ok(Some(dtls)) = sender.transport().await else {
1143
+ return false;
1144
+ };
1145
+ matches!(
1146
+ dtls.ice_transport().get_selected_candidate_pair().await,
1147
+ Ok(Some(pair)) if pair.local().protocol == RTCIceProtocol::Tcp
1148
+ )
1149
+ }
1150
+
1133
1151
  /// Stand in for a connected transport, for a pipeline under test.
1134
1152
  #[cfg(test)]
1135
1153
  pub fn assume_connected(&self) {
@@ -1172,6 +1190,12 @@ impl VideoPeer {
1172
1190
  lock(&self.path).take()
1173
1191
  }
1174
1192
 
1193
+ /// Stand in for transport-wide feedback that reports a path, for a pipeline under test.
1194
+ #[cfg(test)]
1195
+ pub fn report_path(&self, report: PathReport) {
1196
+ *lock(&self.path) = Some(report);
1197
+ }
1198
+
1175
1199
  /// Take the pending "the far end needs a reference frame" request, if any.
1176
1200
  pub fn take_keyframe_request(&self) -> bool {
1177
1201
  self.wants_keyframe.swap(false, Ordering::SeqCst)
@@ -1697,6 +1721,7 @@ pub(crate) mod tests {
1697
1721
  async fn media_receiver(
1698
1722
  receiver: Arc<MediaReceiver>,
1699
1723
  udp: Vec<std::net::SocketAddr>,
1724
+ tcp: Vec<std::net::SocketAddr>,
1700
1725
  ) -> Arc<dyn PeerConnection> {
1701
1726
  let mut media_engine = MediaEngine::default();
1702
1727
  media_engine
@@ -1710,6 +1735,7 @@ pub(crate) mod tests {
1710
1735
  .with_runtime(default_runtime().expect("a runtime"))
1711
1736
  .with_handler(receiver)
1712
1737
  .with_udp_addrs(udp)
1738
+ .with_tcp_addrs(tcp)
1713
1739
  .build()
1714
1740
  .await
1715
1741
  .expect("a receiving peer");
@@ -1719,17 +1745,27 @@ pub(crate) mod tests {
1719
1745
  /// An engine and a client receiving on `client_udp`, offered, answered and
1720
1746
  /// connected over ICE, with the client's RTP arrival times and the
1721
1747
  /// candidates the engine offered.
1722
- struct Connected {
1723
- engine: Arc<VideoPeer>,
1748
+ pub(crate) struct Connected {
1749
+ pub(crate) engine: Arc<VideoPeer>,
1724
1750
  offer: String,
1725
- client: Arc<dyn PeerConnection>,
1751
+ pub(crate) client: Arc<dyn PeerConnection>,
1726
1752
  arrivals: Arc<Mutex<Vec<Instant>>>,
1727
1753
  offered: Arc<Mutex<Vec<String>>>,
1728
1754
  }
1729
1755
 
1730
- async fn connected(client_udp: Vec<std::net::SocketAddr>) -> Connected {
1756
+ /// `client_tcp` and `loopback_tcp` give a pair that runs over ICE-TCP: a
1757
+ /// client with no UDP candidates can only reach the engine over TCP.
1758
+ pub(crate) async fn connected(
1759
+ client_udp: Vec<std::net::SocketAddr>,
1760
+ client_tcp: Vec<std::net::SocketAddr>,
1761
+ loopback_tcp: bool,
1762
+ ) -> Connected {
1731
1763
  let (events, mut events_rx) = tokio::sync::mpsc::unbounded_channel();
1732
- let (engine, offer) = VideoPeer::offer(transport(), events, VideoCodec::Vp9)
1764
+ let options = TransportOptions {
1765
+ loopback_tcp,
1766
+ ..transport()
1767
+ };
1768
+ let (engine, offer) = VideoPeer::offer(options, events, VideoCodec::Vp9)
1733
1769
  .await
1734
1770
  .expect("an offering peer");
1735
1771
  let engine = Arc::new(engine);
@@ -1743,7 +1779,7 @@ pub(crate) mod tests {
1743
1779
  arrivals: Arc::clone(&arrivals),
1744
1780
  connected: Arc::clone(&connected),
1745
1781
  });
1746
- let client = media_receiver(Arc::clone(&receiver), client_udp).await;
1782
+ let client = media_receiver(Arc::clone(&receiver), client_udp, client_tcp).await;
1747
1783
 
1748
1784
  client
1749
1785
  .set_remote_description(RTCSessionDescription::offer(offer.clone()).expect("an offer"))
@@ -1824,7 +1860,12 @@ pub(crate) mod tests {
1824
1860
  client,
1825
1861
  arrivals,
1826
1862
  offered,
1827
- } = connected(vec![std::net::SocketAddr::from(([0, 0, 0, 0], 0))]).await;
1863
+ } = connected(
1864
+ vec![std::net::SocketAddr::from(([0, 0, 0, 0], 0))],
1865
+ Vec::new(),
1866
+ false,
1867
+ )
1868
+ .await;
1828
1869
  eprintln!("restart: the engine offered {:?}", offered.lock().unwrap());
1829
1870
 
1830
1871
  // Any bytes the VP9 packetizer turns into one access unit.
@@ -1968,10 +2009,14 @@ pub(crate) mod tests {
1968
2009
  arrivals,
1969
2010
  offered,
1970
2011
  ..
1971
- } = connected(vec![std::net::SocketAddr::from((
1972
- std::net::Ipv6Addr::LOCALHOST,
1973
- 0,
1974
- ))])
2012
+ } = connected(
2013
+ vec![std::net::SocketAddr::from((
2014
+ std::net::Ipv6Addr::LOCALHOST,
2015
+ 0,
2016
+ ))],
2017
+ Vec::new(),
2018
+ false,
2019
+ )
1975
2020
  .await;
1976
2021
  let offered = offered.lock().unwrap().clone();
1977
2022
  eprintln!("ipv6: the engine offered {offered:?}");
@@ -2047,4 +2092,110 @@ pub(crate) mod tests {
2047
2092
  "no one else is offered a port on the loopback"
2048
2093
  );
2049
2094
  }
2095
+
2096
+ /// A STUN responder bound on the wildcard that answers every Binding
2097
+ /// request with the requester's own address, the way a real STUN server
2098
+ /// would. Reach it at the address `primary_ip` names, not at its bind
2099
+ /// address: the wildcard receives for every interface.
2100
+ async fn stun_responder() -> std::net::SocketAddr {
2101
+ use rtc::stun::message::Setter;
2102
+ let socket = tokio::net::UdpSocket::bind("0.0.0.0:0")
2103
+ .await
2104
+ .expect("a socket");
2105
+ let addr = socket.local_addr().expect("a bound port");
2106
+ tokio::spawn(async move {
2107
+ let mut buffer = vec![0u8; 1500];
2108
+ while let Ok((size, peer)) = socket.recv_from(&mut buffer).await {
2109
+ let mut request = rtc::stun::message::Message::new();
2110
+ request.raw = buffer[..size].to_vec();
2111
+ if request.decode().is_err() {
2112
+ continue;
2113
+ }
2114
+ let mut response = rtc::stun::message::Message::new();
2115
+ response.set_type(rtc::stun::message::BINDING_SUCCESS);
2116
+ response.transaction_id = request.transaction_id;
2117
+ let mapped = rtc::stun::xoraddr::XorMappedAddress {
2118
+ ip: peer.ip(),
2119
+ port: peer.port(),
2120
+ };
2121
+ if mapped.add_to(&mut response).is_err() {
2122
+ continue;
2123
+ }
2124
+ response.write_header();
2125
+ let _ = socket.send_to(&response.raw, peer).await;
2126
+ }
2127
+ });
2128
+ addr
2129
+ }
2130
+
2131
+ /// The interface address this machine sources its own traffic from — the
2132
+ /// address a socket bound to this machine's interfaces can reach. The
2133
+ /// peer binds one socket per non-loopback interface (loopback is skipped
2134
+ /// when host candidates are gathered), and Windows' strong-host model
2135
+ /// refuses to send an interface-bound datagram to 127.0.0.1, where the
2136
+ /// Linux weak-host model allows it — so a loopback STUN fixture gathers
2137
+ /// nothing on Windows. A real STUN server sits on the subnet, and the
2138
+ /// fixture does the same. The probe is a connected UDP socket: the OS
2139
+ /// resolves the route and its source address without sending a packet.
2140
+ fn primary_ip() -> std::net::IpAddr {
2141
+ let probe = std::net::UdpSocket::bind("0.0.0.0:0").and_then(|probe| {
2142
+ probe.connect("192.0.2.1:9")?; // TEST-NET-1: connected UDP sends nothing
2143
+ probe.local_addr()
2144
+ });
2145
+ match probe {
2146
+ Ok(addr) if !addr.ip().is_unspecified() && !addr.ip().is_loopback() => addr.ip(),
2147
+ _ => std::net::IpAddr::from([127, 0, 0, 1]),
2148
+ }
2149
+ }
2150
+
2151
+ /// One ice_servers entry listing two URLs must put both in front of the
2152
+ /// peer connection: the first responder here never answers, so any
2153
+ /// reflexive candidate can only have come from the second URL. The engine
2154
+ /// used to forward only the first URL of each entry, which lost the rest.
2155
+ #[tokio::test]
2156
+ async fn the_second_url_of_an_ice_server_entry_gathers_a_reflexive_candidate() {
2157
+ let ip = primary_ip();
2158
+ // A server that never answers: bound, so its port cannot be reused,
2159
+ // and silent, so it is as good as unreachable.
2160
+ let silent = tokio::net::UdpSocket::bind("0.0.0.0:0")
2161
+ .await
2162
+ .expect("a socket");
2163
+ let unreachable = silent.local_addr().expect("a port");
2164
+ let answering = stun_responder().await;
2165
+
2166
+ let (events, mut events_rx) = tokio::sync::mpsc::unbounded_channel();
2167
+ let (_peer, _offer) = VideoPeer::offer(
2168
+ TransportOptions {
2169
+ ice_servers: vec![(
2170
+ vec![
2171
+ format!("stun:{ip}:{}", unreachable.port()),
2172
+ format!("stun:{ip}:{}", answering.port()),
2173
+ ],
2174
+ None,
2175
+ None,
2176
+ )],
2177
+ loopback_tcp: false,
2178
+ pace_bps: 20_000_000.0,
2179
+ },
2180
+ events,
2181
+ VideoCodec::Vp9,
2182
+ )
2183
+ .await
2184
+ .expect("a peer connection");
2185
+
2186
+ // Host candidates arrive without the network; the reflexive one needs
2187
+ // the binding round trip through the second URL of the entry.
2188
+ let deadline = tokio::time::Instant::now() + Duration::from_secs(10);
2189
+ loop {
2190
+ let event = tokio::time::timeout_at(deadline, events_rx.recv())
2191
+ .await
2192
+ .expect("no reflexive candidate was ever gathered")
2193
+ .expect("the peer event stream ended");
2194
+ if let PeerEvent::Candidate { candidate, .. } = event {
2195
+ if candidate.contains(" typ srflx ") {
2196
+ break;
2197
+ }
2198
+ }
2199
+ }
2200
+ }
2050
2201
  }
@@ -703,7 +703,7 @@ pub struct OpenRequest {
703
703
  pub max_height: usize,
704
704
  pub bitrate_kbps: u32,
705
705
  pub max_fps: u32,
706
- pub ice_servers: Vec<(String, Option<String>, Option<String>)>,
706
+ pub ice_servers: Vec<(Vec<String>, Option<String>, Option<String>)>,
707
707
  pub restore_token: Option<String>,
708
708
  pub ttl: Option<Duration>,
709
709
  /// Offer a passive ICE-TCP candidate on the loopback, for a forwarded client.
@@ -762,6 +762,13 @@ pub enum SessionEvent {
762
762
  KeyframeRequest {
763
763
  generation: u64,
764
764
  },
765
+ /// An ICE server URL the request asked for that the peer's configuration
766
+ /// does not carry: one the WebRTC stack cannot dial (TURN over anything
767
+ /// but UDP). Reported once per open, naming the URL, so a skipped URL is
768
+ /// a warning the consumer sees rather than a silent drop.
769
+ IceServerSkipped {
770
+ url: String,
771
+ },
765
772
  RestoreToken(String),
766
773
  CaptureStopped {
767
774
  reason: String,
@@ -821,6 +828,13 @@ struct Inner {
821
828
  /// closes its Hyprland event socket.
822
829
  #[cfg(target_os = "linux")]
823
830
  window: Mutex<Option<crate::hyprland::WindowLocator>>,
831
+ /// Which Hyprland output the agent cue helper draws on (a located window
832
+ /// share's desktop mode): the helper respawns only when a press lands
833
+ /// elsewhere. Same for the point helper.
834
+ #[cfg(target_os = "linux")]
835
+ cue_output: Mutex<Option<String>>,
836
+ #[cfg(target_os = "linux")]
837
+ point_output: Mutex<Option<String>>,
824
838
  capture: Mutex<Option<FrameSource>>,
825
839
  /// Absent for an encoded source: there is no local keyboard to translate a
826
840
  /// character through, because the client's keys are forwarded instead.
@@ -1274,8 +1288,11 @@ impl Session {
1274
1288
  share_watch.is_some() && source.source_type.as_deref() == Some("Window");
1275
1289
  #[cfg(target_os = "linux")]
1276
1290
  if request.agent_indicator {
1291
+ // No press has landed yet, so no output is chosen: the
1292
+ // compositor places this first helper, and the first
1293
+ // located press moves it onto the landed output.
1277
1294
  indicator =
1278
- crate::indicator::Indicator::start_wayland(source_w, source_h, desktop)
1295
+ crate::indicator::Indicator::start_wayland(source_w, source_h, desktop, None)
1279
1296
  .ok();
1280
1297
  }
1281
1298
  #[cfg(target_os = "macos")]
@@ -1493,6 +1510,10 @@ impl Session {
1493
1510
  },
1494
1511
  #[cfg(target_os = "linux")]
1495
1512
  window: Mutex::new(window),
1513
+ #[cfg(target_os = "linux")]
1514
+ cue_output: Mutex::new(None),
1515
+ #[cfg(target_os = "linux")]
1516
+ point_output: Mutex::new(None),
1496
1517
  capture: Mutex::new(Some(capture)),
1497
1518
  layout: Mutex::new(Some(layout)),
1498
1519
  #[cfg(target_os = "linux")]
@@ -1593,6 +1614,10 @@ impl Session {
1593
1614
  point_desktop: PointDesktop::Unavailable("layer_shell_unavailable"),
1594
1615
  #[cfg(target_os = "linux")]
1595
1616
  window: Mutex::new(None),
1617
+ #[cfg(target_os = "linux")]
1618
+ cue_output: Mutex::new(None),
1619
+ #[cfg(target_os = "linux")]
1620
+ point_output: Mutex::new(None),
1596
1621
  capture: Mutex::new(None),
1597
1622
  layout: Mutex::new(None),
1598
1623
  #[cfg(target_os = "linux")]
@@ -1739,6 +1764,10 @@ impl Session {
1739
1764
  if let Some(helper) = overlay.take() {
1740
1765
  helper.stop();
1741
1766
  }
1767
+ #[cfg(target_os = "linux")]
1768
+ {
1769
+ *lock(&self.inner.point_output) = None;
1770
+ }
1742
1771
  return Ok(());
1743
1772
  }
1744
1773
  let (x, y) = params
@@ -1763,21 +1792,35 @@ impl Session {
1763
1792
  ));
1764
1793
  }
1765
1794
  // A located window's point shows where a press there would land, on
1766
- // the output holding it; the next may land on another output, so each
1767
- // gets its own helper.
1795
+ // the output holding it; the helper moves only when that output
1796
+ // changes, so repeats on one output reuse it with no new process.
1768
1797
  #[cfg(target_os = "linux")]
1769
1798
  let landed = match lock(&self.inner.window).as_ref() {
1770
1799
  Some(window) => {
1771
1800
  let (px, py) = window
1772
1801
  .locate(x, y)
1773
1802
  .map_err(|message| SessionError::new("indicator-unavailable", message))?;
1774
- if let Some(helper) = overlay.take() {
1775
- helper.stop();
1776
- }
1777
1803
  Some((px.floor() as i64, py.floor() as i64))
1778
1804
  }
1779
1805
  None => None,
1780
1806
  };
1807
+ #[cfg(target_os = "linux")]
1808
+ let landed_output: Option<String> = match landed {
1809
+ Some((px, py)) => {
1810
+ crate::hyprland::monitor_containing(px as f64, py as f64).unwrap_or(None)
1811
+ }
1812
+ None => None,
1813
+ };
1814
+ #[cfg(target_os = "linux")]
1815
+ if match (&landed, &landed_output) {
1816
+ (Some(_), output) => lock(&self.inner.point_output).clone() != *output,
1817
+ (None, _) => overlay.is_none(),
1818
+ } {
1819
+ if let Some(helper) = overlay.take() {
1820
+ helper.stop();
1821
+ }
1822
+ *lock(&self.inner.point_output) = landed_output.clone();
1823
+ }
1781
1824
  #[cfg(not(target_os = "windows"))]
1782
1825
  if overlay.is_none() {
1783
1826
  #[cfg(target_os = "linux")]
@@ -2139,6 +2182,11 @@ impl Inner {
2139
2182
  if let Some(helper) = lock(&self.point_overlay).take() {
2140
2183
  helper.stop();
2141
2184
  }
2185
+ #[cfg(target_os = "linux")]
2186
+ {
2187
+ *lock(&self.cue_output) = None;
2188
+ *lock(&self.point_output) = None;
2189
+ }
2142
2190
  // Release first, then tear down: a stuck modifier is the one failure the
2143
2191
  // user cannot undo by reconnecting.
2144
2192
  if let Ok(mut input) = self.input.lock() {
@@ -2318,6 +2366,10 @@ impl Inner {
2318
2366
  self.reject(seq, "input-unavailable", "no input backend");
2319
2367
  return;
2320
2368
  }
2369
+ // Only a located window-share press can move the cue helper: other
2370
+ // paths have no desktop point to choose an output from.
2371
+ #[cfg(target_os = "linux")]
2372
+ let located_press = located && matches!(&message, ControlMessage::Pointer { .. });
2321
2373
  let outcome = match message {
2322
2374
  ControlMessage::Pointer {
2323
2375
  phase,
@@ -2361,6 +2413,14 @@ impl Inner {
2361
2413
  };
2362
2414
  match outcome {
2363
2415
  Ok(()) => {
2416
+ #[cfg(target_os = "linux")]
2417
+ if located_press {
2418
+ // Onto the landed output before the cue is told where to
2419
+ // draw, so a cross-output tap draws there, not nowhere.
2420
+ if let Some((_, x, y)) = feedback {
2421
+ self.move_cue_to(x, y);
2422
+ }
2423
+ }
2364
2424
  if let Some((kind, x, y)) = feedback {
2365
2425
  if let Ok(mut indicator) = self.indicator.lock() {
2366
2426
  if let Some(indicator) = indicator.as_mut() {
@@ -2426,6 +2486,49 @@ impl Inner {
2426
2486
  }
2427
2487
  }
2428
2488
 
2489
+ /// Move the agent cue helper onto the output holding desktop point
2490
+ /// `(x, y)`, where a located press just landed. A press on the helper's
2491
+ /// own output reuses it with no new process; anything else respawns it
2492
+ /// with the landed origin, which selects that output through the point
2493
+ /// helper's xdg-output selection. Best-effort: when the output or the
2494
+ /// respawn is unavailable, the standing helper keeps the cue.
2495
+ #[cfg(target_os = "linux")]
2496
+ fn move_cue_to(&self, x: i64, y: i64) {
2497
+ let output = match crate::hyprland::monitor_containing(x as f64, y as f64) {
2498
+ Ok(output) => output,
2499
+ Err(error) => {
2500
+ log::warn!("agent cue stays on its output: {error}");
2501
+ return;
2502
+ }
2503
+ };
2504
+ if lock(&self.cue_output).clone() == output {
2505
+ return;
2506
+ }
2507
+ let Some(name) = output else { return };
2508
+ let mut indicator = lock(&self.indicator);
2509
+ if let Some(helper) = indicator.take() {
2510
+ helper.stop();
2511
+ }
2512
+ let (width, height) = (
2513
+ self.source.width.max(1) as usize,
2514
+ self.source.height.max(1) as usize,
2515
+ );
2516
+ match crate::indicator::Indicator::start_wayland(
2517
+ width,
2518
+ height,
2519
+ true,
2520
+ Some((x as i32, y as i32)),
2521
+ ) {
2522
+ Ok(helper) => {
2523
+ *indicator = Some(helper);
2524
+ *lock(&self.cue_output) = Some(name);
2525
+ }
2526
+ Err(error) => {
2527
+ log::warn!("agent cue stays on its output: the {name} helper did not start: {error:#}");
2528
+ }
2529
+ }
2530
+ }
2531
+
2429
2532
  fn pointer(
2430
2533
  &self,
2431
2534
  phase: PointerPhase,
@@ -2804,6 +2907,12 @@ fn auto_bitrate(width: usize, height: usize) -> u32 {
2804
2907
  const QUEUE_HIGH: Duration = Duration::from_millis(25);
2805
2908
  /// Below this the path has room again and the target may creep back up.
2806
2909
  const QUEUE_LOW: Duration = Duration::from_millis(10);
2910
+ /// The same thresholds for a TCP path. TCP never drops, so its queue is read
2911
+ /// as delay alone, and a relay's bridge adds 20-30 ms of delivery jitter that is
2912
+ /// not queueing: at 25/10 ms that jitter backs the target off to its floor
2913
+ /// with the link idle. Only a queue that stands over 100 ms is a backlog here.
2914
+ const QUEUE_HIGH_TCP: Duration = Duration::from_millis(100);
2915
+ const QUEUE_LOW_TCP: Duration = Duration::from_millis(60);
2807
2916
  /// How often queueing delay may back the target off: long enough for the last
2808
2917
  /// back-off to reach the far end and show in its feedback, short enough that a
2809
2918
  /// queue is drained in a few steps rather than a few seconds.
@@ -2825,6 +2934,8 @@ struct RateControl {
2825
2934
  last_change: Option<Instant>,
2826
2935
  /// The last queueing delay reported, while the path reports one.
2827
2936
  queue: Option<Duration>,
2937
+ queue_high: Duration,
2938
+ queue_low: Duration,
2828
2939
  }
2829
2940
 
2830
2941
  impl RateControl {
@@ -2843,9 +2954,21 @@ impl RateControl {
2843
2954
  target: ceiling,
2844
2955
  last_change: None,
2845
2956
  queue: None,
2957
+ queue_high: QUEUE_HIGH,
2958
+ queue_low: QUEUE_LOW,
2846
2959
  }
2847
2960
  }
2848
2961
 
2962
+ /// Read queueing delay against the thresholds for the transport the selected
2963
+ /// pair runs over.
2964
+ fn set_tcp(&mut self, tcp: bool) {
2965
+ (self.queue_high, self.queue_low) = if tcp {
2966
+ (QUEUE_HIGH_TCP, QUEUE_LOW_TCP)
2967
+ } else {
2968
+ (QUEUE_HIGH, QUEUE_LOW)
2969
+ };
2970
+ }
2971
+
2849
2972
  /// The new target when these loss reports (RFC 3550 fractions, /256) or
2850
2973
  /// this path report move it: down on a standing queue at most every
2851
2974
  /// `DELAY_BACKOFF_EVERY`, otherwise at most once a second.
@@ -2856,7 +2979,7 @@ impl RateControl {
2856
2979
  let since = self.last_change.map(|at| now.duration_since(at));
2857
2980
  let waited = |gap: Duration| since.is_none_or(|since| since >= gap);
2858
2981
  let floor = (self.ceiling / 8).max(500);
2859
- let next = if let Some(path) = path.filter(|path| path.queue_delay > QUEUE_HIGH) {
2982
+ let next = if let Some(path) = path.filter(|path| path.queue_delay > self.queue_high) {
2860
2983
  if !waited(DELAY_BACKOFF_EVERY) {
2861
2984
  return None;
2862
2985
  }
@@ -2868,7 +2991,7 @@ impl RateControl {
2868
2991
  return None;
2869
2992
  }
2870
2993
  let loss = worst as f64 / 256.0;
2871
- let roomy = self.queue.is_none_or(|queue| queue < QUEUE_LOW);
2994
+ let roomy = self.queue.is_none_or(|queue| queue < self.queue_low);
2872
2995
  if loss > 0.10 {
2873
2996
  ((self.target as f64 * 0.7) as u32).max(floor)
2874
2997
  } else if loss < 0.02 && roomy {
@@ -3117,6 +3240,9 @@ fn spawn_pipeline(
3117
3240
  }
3118
3241
  let reports = peer.take_loss_reports();
3119
3242
  let path = peer.take_path_report();
3243
+ if path.is_some() {
3244
+ rate.set_tcp(handle.block_on(peer.selected_pair_is_tcp()));
3245
+ }
3120
3246
  if let Some(kbps) = rate.update(&reports, path, Instant::now()) {
3121
3247
  let applied = inner.encoder.lock().map(|mut encoder| {
3122
3248
  encoder
@@ -3678,6 +3804,10 @@ mod tests {
3678
3804
  point_desktop: PointDesktop::Unavailable("layer_shell_unavailable"),
3679
3805
  #[cfg(target_os = "linux")]
3680
3806
  window: Mutex::new(None),
3807
+ #[cfg(target_os = "linux")]
3808
+ cue_output: Mutex::new(None),
3809
+ #[cfg(target_os = "linux")]
3810
+ point_output: Mutex::new(None),
3681
3811
  capture: Mutex::new(None),
3682
3812
  layout: Mutex::new(None),
3683
3813
  #[cfg(target_os = "linux")]
@@ -3757,7 +3887,7 @@ mod tests {
3757
3887
  )
3758
3888
  .await
3759
3889
  .expect("a peer connection builds without a network");
3760
- test_inner_with(events, peer, 64, 64)
3890
+ test_inner_with(events, Arc::new(peer), 64, 64)
3761
3891
  }
3762
3892
 
3763
3893
  #[cfg(target_os = "linux")]
@@ -3892,7 +4022,7 @@ mod tests {
3892
4022
 
3893
4023
  fn test_inner_with(
3894
4024
  events: tokio_mpsc::UnboundedSender<Notice>,
3895
- peer: VideoPeer,
4025
+ peer: Arc<VideoPeer>,
3896
4026
  width: usize,
3897
4027
  height: usize,
3898
4028
  ) -> (Arc<Inner>, Arc<Mutex<Vec<(i16, bool)>>>) {
@@ -3920,7 +4050,7 @@ mod tests {
3920
4050
  latest: Arc::new(Mutex::new(None)),
3921
4051
  capture_error: Arc::new(Mutex::new(None)),
3922
4052
  frame_changes: tokio::sync::watch::channel(0).1,
3923
- peer: Mutex::new(Some(Arc::new(peer))),
4053
+ peer: Mutex::new(Some(peer)),
3924
4054
  encoder: Mutex::new(Some(VideoEncoder::Vp9(
3925
4055
  Encoder::new(width, height, 1000, 30, 1).expect("an encoder"),
3926
4056
  ))),
@@ -3939,6 +4069,10 @@ mod tests {
3939
4069
  point_desktop: PointDesktop::Unavailable("layer_shell_unavailable"),
3940
4070
  #[cfg(target_os = "linux")]
3941
4071
  window: Mutex::new(None),
4072
+ #[cfg(target_os = "linux")]
4073
+ cue_output: Mutex::new(None),
4074
+ #[cfg(target_os = "linux")]
4075
+ point_output: Mutex::new(None),
3942
4076
  layout: Mutex::new(Some(Layout::from_environment().expect("a keymap"))),
3943
4077
  #[cfg(target_os = "linux")]
3944
4078
  audio: Mutex::new(None),
@@ -4518,7 +4652,7 @@ mod tests {
4518
4652
  let codec = crate::peer::tests::h264_receiver_codec();
4519
4653
  let (peer, _) = crate::peer::tests::answered(vec![codec.clone()]).await;
4520
4654
  let (events, mut received) = tokio_mpsc::unbounded_channel();
4521
- let (inner, _) = test_inner_with(events, peer, 64, 64);
4655
+ let (inner, _) = test_inner_with(events, Arc::new(peer), 64, 64);
4522
4656
  let session = Session { inner };
4523
4657
  let mut offers = Vec::new();
4524
4658
  for _ in 0..3 {
@@ -4690,6 +4824,72 @@ mod tests {
4690
4824
  assert_eq!((later.codec, later.encoder), ("vp9", "libvpx"));
4691
4825
  }
4692
4826
 
4827
+ /// The rate target after 3 s of delivery jitter on a connected pair: a queue
4828
+ /// that alternates between 5 and 30 ms with nothing lost, and 800 kbps
4829
+ /// delivered. The pipeline runs for real; only the path reports are fed.
4830
+ async fn target_under_bridge_jitter(tcp: bool) -> u32 {
4831
+ let peers = crate::peer::tests::connected(
4832
+ if tcp {
4833
+ Vec::new()
4834
+ } else {
4835
+ vec![std::net::SocketAddr::from(([0, 0, 0, 0], 0))]
4836
+ },
4837
+ if tcp {
4838
+ vec![std::net::SocketAddr::from(([127, 0, 0, 1], 0))]
4839
+ } else {
4840
+ Vec::new()
4841
+ },
4842
+ tcp,
4843
+ )
4844
+ .await;
4845
+ let engine = Arc::clone(&peers.engine);
4846
+ assert_eq!(
4847
+ engine.selected_pair_is_tcp().await,
4848
+ tcp,
4849
+ "the selected pair must run over {}",
4850
+ if tcp { "TCP" } else { "UDP" }
4851
+ );
4852
+ let (events, _events_rx) = tokio_mpsc::unbounded_channel();
4853
+ let (inner, _recorded) = test_inner_with(events, Arc::clone(&engine), 64, 64);
4854
+ let (_frame_tx, frame_rx) = latest_frame();
4855
+ spawn_pipeline(
4856
+ &inner,
4857
+ Arc::clone(&engine),
4858
+ frame_rx,
4859
+ Arc::new(AtomicBool::new(true)),
4860
+ 30,
4861
+ 1000,
4862
+ );
4863
+ for step in 0..300 {
4864
+ let queue_delay = Duration::from_millis(if step % 2 == 0 { 5 } else { 30 });
4865
+ engine.report_path(PathReport {
4866
+ queue_delay,
4867
+ delivered_kbps: 800,
4868
+ });
4869
+ tokio::time::sleep(Duration::from_millis(10)).await;
4870
+ }
4871
+ let target = inner.metrics.lock().unwrap().target_kbps;
4872
+ drop(peers);
4873
+ target
4874
+ }
4875
+
4876
+ #[tokio::test(flavor = "multi_thread")]
4877
+ async fn a_tcp_path_with_bridge_jitter_holds_its_rate_ceiling() {
4878
+ assert_eq!(
4879
+ target_under_bridge_jitter(true).await,
4880
+ 1000,
4881
+ "a 5-30 ms jitter that is not a queue must not back a TCP path off"
4882
+ );
4883
+ }
4884
+
4885
+ #[tokio::test(flavor = "multi_thread")]
4886
+ async fn a_udp_path_with_the_same_jitter_still_backs_off() {
4887
+ assert!(
4888
+ target_under_bridge_jitter(false).await < 1000,
4889
+ "a UDP path reads the same 30 ms as a standing queue and must back off"
4890
+ );
4891
+ }
4892
+
4693
4893
  #[tokio::test]
4694
4894
  async fn a_receiver_that_asks_for_h264_is_sent_h264_from_the_first_frame() {
4695
4895
  let Some(info) = tokio::task::spawn_blocking(crate::h264::probe)
@@ -4702,7 +4902,7 @@ mod tests {
4702
4902
  let (peer, _answer) =
4703
4903
  crate::peer::tests::answered(vec![crate::peer::tests::h264_receiver_codec()]).await;
4704
4904
  let (events, _events_rx) = tokio_mpsc::unbounded_channel();
4705
- let (inner, _recorded) = test_inner_with(events, peer, 320, 240);
4905
+ let (inner, _recorded) = test_inner_with(events, Arc::new(peer), 320, 240);
4706
4906
  // The answer is real, so ICE runs and, with no candidates, never
4707
4907
  // connects; each state it reports would take back `assume_connected`.
4708
4908
  let pinned = current_peer(&inner).expect("the test's peer");
@@ -97,8 +97,8 @@ No public API changed; no signalling or wire change.
97
97
 
98
98
  Do this once upstream rtc-ice keeps the previous selected pair across a restart
99
99
  on its own and nominates a freshly validated pair without waiting for the next
100
- check tick (an upstream PR is a follow-up; it needs the captain's word first,
101
- since it would carry his name).
100
+ check tick (an upstream PR is a follow-up; that needs a maintainer decision first,
101
+ since the contribution would carry its author's name).
102
102
 
103
103
  ## Dev-only measurement instrumentation (not part of the patch)
104
104
 
@@ -10,7 +10,7 @@ use stun::textattrs::*;
10
10
 
11
11
  use crate::attributes::{control::*, priority::*, use_candidate::*};
12
12
  use crate::candidate::{candidate_pair::*, *};
13
- // desklink scout trace (dl-roam-postnom-attr):
13
+ // desklink scout trace: dev-only ICE timeline instrumentation.
14
14
  use crate::dl_trace;
15
15
  use crate::dl_trace::dl_ice;
16
16
 
@@ -38,7 +38,7 @@ use stun::xoraddr::*;
38
38
 
39
39
  use crate::candidate::candidate_peer_reflexive::CandidatePeerReflexiveConfig;
40
40
  use crate::candidate::{candidate_pair::*, *};
41
- // desklink scout trace (dl-roam-postnom-attr):
41
+ // desklink scout trace: dev-only ICE timeline instrumentation.
42
42
  use crate::dl_trace;
43
43
  use crate::dl_trace::dl_ice;
44
44
  use crate::mdns::{MulticastDnsMode, create_multicast_dns, generate_multicast_dns_name};
@@ -1,7 +1,7 @@
1
1
  //! desklink scout instrumentation (dev-only, `DESKLINK_ICE_TRACE=1`).
2
2
  //!
3
3
  //! Timestamped stderr trace of the ICE agent's restart/check/nomination timeline
4
- //! for task dl-roam-postnom-attr. Not part of the shipped patch; see
4
+ //! for ICE restart diagnosis. Not part of the shipped patch; see
5
5
  //! DESKLINK-PATCH.md. Every call site is marked `// desklink scout trace:` so the
6
6
  //! whole diff is trivially droppable.
7
7
 
@@ -58,7 +58,7 @@
58
58
  /// without adding — and version-matching — a direct `rtc-crypto` dependency.
59
59
  pub use crypto;
60
60
 
61
- // desklink scout trace: dev-only ICE timeline instrumentation (dl-roam-postnom-attr).
61
+ // desklink scout trace: dev-only ICE timeline instrumentation.
62
62
  pub(crate) mod dl_trace;
63
63
 
64
64
  pub mod agent;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@desklink/host",
3
- "version": "0.5.8",
3
+ "version": "0.5.9",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",
@@ -38,7 +38,7 @@
38
38
  "ws": "8.21.3"
39
39
  },
40
40
  "optionalDependencies": {
41
- "@desklink/host-linux-x64-gnu": "0.5.8",
42
- "@desklink/host-darwin-arm64": "0.5.8"
41
+ "@desklink/host-linux-x64-gnu": "0.5.9",
42
+ "@desklink/host-darwin-arm64": "0.5.9"
43
43
  }
44
44
  }