@desklink/host 0.5.7 → 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.
- package/dist/protocol.d.ts +1 -0
- package/dist/protocol.d.ts.map +1 -1
- package/docs/PROTOCOL.md +49 -5
- package/engine/Cargo.lock +2 -1
- package/engine/Cargo.toml +5 -2
- package/engine/native/agent_overlay_wayland.c +46 -18
- package/engine/src/hyprland.rs +38 -3
- package/engine/src/indicator.rs +34 -10
- package/engine/src/main.rs +115 -15
- package/engine/src/peer.rs +290 -18
- package/engine/src/session.rs +257 -23
- package/engine/vendor/rtc-ice/DESKLINK-PATCH.md +2 -2
- package/engine/vendor/rtc-ice/src/agent/agent_selector.rs +1 -1
- package/engine/vendor/rtc-ice/src/agent/mod.rs +1 -1
- package/engine/vendor/rtc-ice/src/dl_trace.rs +1 -1
- package/engine/vendor/rtc-ice/src/lib.rs +1 -1
- package/package.json +3 -3
package/engine/src/session.rs
CHANGED
|
@@ -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
|
|
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.
|
|
@@ -1267,9 +1281,19 @@ impl Session {
|
|
|
1267
1281
|
let source_h = source.height.max(1) as usize;
|
|
1268
1282
|
let (width, height) =
|
|
1269
1283
|
fit(source_w, source_h, request.max_width, request.max_height);
|
|
1284
|
+
// A Hyprland window share's cues go to the desktop point each
|
|
1285
|
+
// press lands on, and its picture holds no layer surface to mask.
|
|
1286
|
+
#[cfg(target_os = "linux")]
|
|
1287
|
+
let desktop =
|
|
1288
|
+
share_watch.is_some() && source.source_type.as_deref() == Some("Window");
|
|
1270
1289
|
#[cfg(target_os = "linux")]
|
|
1271
1290
|
if request.agent_indicator {
|
|
1272
|
-
|
|
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.
|
|
1294
|
+
indicator =
|
|
1295
|
+
crate::indicator::Indicator::start_wayland(source_w, source_h, desktop, None)
|
|
1296
|
+
.ok();
|
|
1273
1297
|
}
|
|
1274
1298
|
#[cfg(target_os = "macos")]
|
|
1275
1299
|
if request.agent_indicator {
|
|
@@ -1312,6 +1336,7 @@ impl Session {
|
|
|
1312
1336
|
sink,
|
|
1313
1337
|
indicator
|
|
1314
1338
|
.as_ref()
|
|
1339
|
+
.filter(|_| !desktop)
|
|
1315
1340
|
.map(crate::indicator::Indicator::position),
|
|
1316
1341
|
Box::new({
|
|
1317
1342
|
let status = capture_status.clone();
|
|
@@ -1485,6 +1510,10 @@ impl Session {
|
|
|
1485
1510
|
},
|
|
1486
1511
|
#[cfg(target_os = "linux")]
|
|
1487
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),
|
|
1488
1517
|
capture: Mutex::new(Some(capture)),
|
|
1489
1518
|
layout: Mutex::new(Some(layout)),
|
|
1490
1519
|
#[cfg(target_os = "linux")]
|
|
@@ -1585,6 +1614,10 @@ impl Session {
|
|
|
1585
1614
|
point_desktop: PointDesktop::Unavailable("layer_shell_unavailable"),
|
|
1586
1615
|
#[cfg(target_os = "linux")]
|
|
1587
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),
|
|
1588
1621
|
capture: Mutex::new(None),
|
|
1589
1622
|
layout: Mutex::new(None),
|
|
1590
1623
|
#[cfg(target_os = "linux")]
|
|
@@ -1731,6 +1764,10 @@ impl Session {
|
|
|
1731
1764
|
if let Some(helper) = overlay.take() {
|
|
1732
1765
|
helper.stop();
|
|
1733
1766
|
}
|
|
1767
|
+
#[cfg(target_os = "linux")]
|
|
1768
|
+
{
|
|
1769
|
+
*lock(&self.inner.point_output) = None;
|
|
1770
|
+
}
|
|
1734
1771
|
return Ok(());
|
|
1735
1772
|
}
|
|
1736
1773
|
let (x, y) = params
|
|
@@ -1754,6 +1791,36 @@ impl Session {
|
|
|
1754
1791
|
"timeout must be 1..120000 ms; label must be at most 96 printable ASCII bytes",
|
|
1755
1792
|
));
|
|
1756
1793
|
}
|
|
1794
|
+
// A located window's point shows where a press there would land, on
|
|
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.
|
|
1797
|
+
#[cfg(target_os = "linux")]
|
|
1798
|
+
let landed = match lock(&self.inner.window).as_ref() {
|
|
1799
|
+
Some(window) => {
|
|
1800
|
+
let (px, py) = window
|
|
1801
|
+
.locate(x, y)
|
|
1802
|
+
.map_err(|message| SessionError::new("indicator-unavailable", message))?;
|
|
1803
|
+
Some((px.floor() as i64, py.floor() as i64))
|
|
1804
|
+
}
|
|
1805
|
+
None => None,
|
|
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
|
+
}
|
|
1757
1824
|
#[cfg(not(target_os = "windows"))]
|
|
1758
1825
|
if overlay.is_none() {
|
|
1759
1826
|
#[cfg(target_os = "linux")]
|
|
@@ -1762,7 +1829,10 @@ impl Session {
|
|
|
1762
1829
|
PointDesktop::Wayland => crate::indicator::Indicator::start_point_wayland(
|
|
1763
1830
|
source.width,
|
|
1764
1831
|
source.height,
|
|
1765
|
-
|
|
1832
|
+
landed
|
|
1833
|
+
.map(|(x, y)| (x as i32, y as i32))
|
|
1834
|
+
.or(source.position),
|
|
1835
|
+
landed.is_some(),
|
|
1766
1836
|
),
|
|
1767
1837
|
PointDesktop::Unavailable(_) => unreachable!("validated point target"),
|
|
1768
1838
|
};
|
|
@@ -1784,6 +1854,8 @@ impl Session {
|
|
|
1784
1854
|
}
|
|
1785
1855
|
})?);
|
|
1786
1856
|
}
|
|
1857
|
+
#[cfg(target_os = "linux")]
|
|
1858
|
+
let (x, y) = landed.unwrap_or((x, y));
|
|
1787
1859
|
#[cfg(not(target_os = "windows"))]
|
|
1788
1860
|
return overlay
|
|
1789
1861
|
.as_mut()
|
|
@@ -2110,6 +2182,11 @@ impl Inner {
|
|
|
2110
2182
|
if let Some(helper) = lock(&self.point_overlay).take() {
|
|
2111
2183
|
helper.stop();
|
|
2112
2184
|
}
|
|
2185
|
+
#[cfg(target_os = "linux")]
|
|
2186
|
+
{
|
|
2187
|
+
*lock(&self.cue_output) = None;
|
|
2188
|
+
*lock(&self.point_output) = None;
|
|
2189
|
+
}
|
|
2113
2190
|
// Release first, then tear down: a stuck modifier is the one failure the
|
|
2114
2191
|
// user cannot undo by reconnecting.
|
|
2115
2192
|
if let Ok(mut input) = self.input.lock() {
|
|
@@ -2239,7 +2316,7 @@ impl Inner {
|
|
|
2239
2316
|
}
|
|
2240
2317
|
*last = seq;
|
|
2241
2318
|
}
|
|
2242
|
-
let feedback = match &message {
|
|
2319
|
+
let mut feedback = match &message {
|
|
2243
2320
|
ControlMessage::Pointer { phase, x, y, .. } if lock(&self.encoded).is_none() => {
|
|
2244
2321
|
let (x, y) = to_source_pixels(*x, *y, self.encoded_size(), &self.source);
|
|
2245
2322
|
Some((
|
|
@@ -2271,11 +2348,6 @@ impl Inner {
|
|
|
2271
2348
|
let located = lock(&self.window).is_some();
|
|
2272
2349
|
#[cfg(not(target_os = "linux"))]
|
|
2273
2350
|
let located = false;
|
|
2274
|
-
let feedback = if located {
|
|
2275
|
-
feedback.filter(|(kind, ..)| *kind == 'T')
|
|
2276
|
-
} else {
|
|
2277
|
-
feedback
|
|
2278
|
-
};
|
|
2279
2351
|
if self.source.source_type.as_deref() == Some("Window")
|
|
2280
2352
|
&& !located
|
|
2281
2353
|
&& matches!(&message, ControlMessage::Pointer { phase, .. } if !matches!(phase, PointerPhase::Cancel))
|
|
@@ -2294,6 +2366,10 @@ impl Inner {
|
|
|
2294
2366
|
self.reject(seq, "input-unavailable", "no input backend");
|
|
2295
2367
|
return;
|
|
2296
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 { .. });
|
|
2297
2373
|
let outcome = match message {
|
|
2298
2374
|
ControlMessage::Pointer {
|
|
2299
2375
|
phase,
|
|
@@ -2301,7 +2377,14 @@ impl Inner {
|
|
|
2301
2377
|
y,
|
|
2302
2378
|
button,
|
|
2303
2379
|
..
|
|
2304
|
-
} => self.pointer(phase, x, y, button, seq)
|
|
2380
|
+
} => self.pointer(phase, x, y, button, seq).map(|landed| {
|
|
2381
|
+
// A located window's cue goes where the press landed, or nowhere.
|
|
2382
|
+
if located {
|
|
2383
|
+
feedback = feedback
|
|
2384
|
+
.zip(landed)
|
|
2385
|
+
.map(|((kind, ..), (x, y))| (kind, x, y));
|
|
2386
|
+
}
|
|
2387
|
+
}),
|
|
2305
2388
|
ControlMessage::Wheel { dx, dy, .. } => self.wheel(dx, dy, seq),
|
|
2306
2389
|
ControlMessage::Key {
|
|
2307
2390
|
name,
|
|
@@ -2330,6 +2413,14 @@ impl Inner {
|
|
|
2330
2413
|
};
|
|
2331
2414
|
match outcome {
|
|
2332
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
|
+
}
|
|
2333
2424
|
if let Some((kind, x, y)) = feedback {
|
|
2334
2425
|
if let Ok(mut indicator) = self.indicator.lock() {
|
|
2335
2426
|
if let Some(indicator) = indicator.as_mut() {
|
|
@@ -2395,6 +2486,49 @@ impl Inner {
|
|
|
2395
2486
|
}
|
|
2396
2487
|
}
|
|
2397
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
|
+
|
|
2398
2532
|
fn pointer(
|
|
2399
2533
|
&self,
|
|
2400
2534
|
phase: PointerPhase,
|
|
@@ -2402,7 +2536,7 @@ impl Inner {
|
|
|
2402
2536
|
y: i64,
|
|
2403
2537
|
button: i64,
|
|
2404
2538
|
_seq: u64,
|
|
2405
|
-
) -> std::result::Result<(
|
|
2539
|
+
) -> std::result::Result<Option<(i64, i64)>, (&'static str, String)> {
|
|
2406
2540
|
let (width, height) = self.encoded_size();
|
|
2407
2541
|
if !(0..width as i64).contains(&x) || !(0..height as i64).contains(&y) {
|
|
2408
2542
|
return Err((
|
|
@@ -2421,19 +2555,21 @@ impl Inner {
|
|
|
2421
2555
|
.place(x, y)
|
|
2422
2556
|
.map_err(|message| ("input-unavailable", message))
|
|
2423
2557
|
};
|
|
2558
|
+
// The desktop point the pointer landed on, where its cue belongs.
|
|
2424
2559
|
return match phase {
|
|
2425
|
-
PointerPhase::Move => place(),
|
|
2560
|
+
PointerPhase::Move => place().map(Some),
|
|
2426
2561
|
PointerPhase::Down => {
|
|
2427
2562
|
// Refused before the pointer moves: nothing could press.
|
|
2428
2563
|
self.with_input(|_| Ok(()))?;
|
|
2429
|
-
place()?;
|
|
2430
|
-
self.with_input(|target| target.button(Button::from_number(button), true))
|
|
2564
|
+
let point = place()?;
|
|
2565
|
+
self.with_input(|target| target.button(Button::from_number(button), true))?;
|
|
2566
|
+
Ok(Some(point))
|
|
2431
2567
|
}
|
|
2432
2568
|
PointerPhase::Up => {
|
|
2433
2569
|
self.with_input(|target| target.button(Button::from_number(button), false))?;
|
|
2434
|
-
place()
|
|
2570
|
+
place().map(Some)
|
|
2435
2571
|
}
|
|
2436
|
-
PointerPhase::Cancel => self.with_input(InputTarget::release_all),
|
|
2572
|
+
PointerPhase::Cancel => self.with_input(InputTarget::release_all).map(|()| None),
|
|
2437
2573
|
};
|
|
2438
2574
|
}
|
|
2439
2575
|
self.with_input(|target| match phase {
|
|
@@ -2448,6 +2584,7 @@ impl Inner {
|
|
|
2448
2584
|
}
|
|
2449
2585
|
PointerPhase::Cancel => target.release_all(),
|
|
2450
2586
|
})
|
|
2587
|
+
.map(|()| None)
|
|
2451
2588
|
}
|
|
2452
2589
|
|
|
2453
2590
|
fn wheel(
|
|
@@ -2770,6 +2907,12 @@ fn auto_bitrate(width: usize, height: usize) -> u32 {
|
|
|
2770
2907
|
const QUEUE_HIGH: Duration = Duration::from_millis(25);
|
|
2771
2908
|
/// Below this the path has room again and the target may creep back up.
|
|
2772
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);
|
|
2773
2916
|
/// How often queueing delay may back the target off: long enough for the last
|
|
2774
2917
|
/// back-off to reach the far end and show in its feedback, short enough that a
|
|
2775
2918
|
/// queue is drained in a few steps rather than a few seconds.
|
|
@@ -2791,6 +2934,8 @@ struct RateControl {
|
|
|
2791
2934
|
last_change: Option<Instant>,
|
|
2792
2935
|
/// The last queueing delay reported, while the path reports one.
|
|
2793
2936
|
queue: Option<Duration>,
|
|
2937
|
+
queue_high: Duration,
|
|
2938
|
+
queue_low: Duration,
|
|
2794
2939
|
}
|
|
2795
2940
|
|
|
2796
2941
|
impl RateControl {
|
|
@@ -2809,9 +2954,21 @@ impl RateControl {
|
|
|
2809
2954
|
target: ceiling,
|
|
2810
2955
|
last_change: None,
|
|
2811
2956
|
queue: None,
|
|
2957
|
+
queue_high: QUEUE_HIGH,
|
|
2958
|
+
queue_low: QUEUE_LOW,
|
|
2812
2959
|
}
|
|
2813
2960
|
}
|
|
2814
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
|
+
|
|
2815
2972
|
/// The new target when these loss reports (RFC 3550 fractions, /256) or
|
|
2816
2973
|
/// this path report move it: down on a standing queue at most every
|
|
2817
2974
|
/// `DELAY_BACKOFF_EVERY`, otherwise at most once a second.
|
|
@@ -2822,7 +2979,7 @@ impl RateControl {
|
|
|
2822
2979
|
let since = self.last_change.map(|at| now.duration_since(at));
|
|
2823
2980
|
let waited = |gap: Duration| since.is_none_or(|since| since >= gap);
|
|
2824
2981
|
let floor = (self.ceiling / 8).max(500);
|
|
2825
|
-
let next = if let Some(path) = path.filter(|path| path.queue_delay >
|
|
2982
|
+
let next = if let Some(path) = path.filter(|path| path.queue_delay > self.queue_high) {
|
|
2826
2983
|
if !waited(DELAY_BACKOFF_EVERY) {
|
|
2827
2984
|
return None;
|
|
2828
2985
|
}
|
|
@@ -2834,7 +2991,7 @@ impl RateControl {
|
|
|
2834
2991
|
return None;
|
|
2835
2992
|
}
|
|
2836
2993
|
let loss = worst as f64 / 256.0;
|
|
2837
|
-
let roomy = self.queue.is_none_or(|queue| queue <
|
|
2994
|
+
let roomy = self.queue.is_none_or(|queue| queue < self.queue_low);
|
|
2838
2995
|
if loss > 0.10 {
|
|
2839
2996
|
((self.target as f64 * 0.7) as u32).max(floor)
|
|
2840
2997
|
} else if loss < 0.02 && roomy {
|
|
@@ -3083,6 +3240,9 @@ fn spawn_pipeline(
|
|
|
3083
3240
|
}
|
|
3084
3241
|
let reports = peer.take_loss_reports();
|
|
3085
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
|
+
}
|
|
3086
3246
|
if let Some(kbps) = rate.update(&reports, path, Instant::now()) {
|
|
3087
3247
|
let applied = inner.encoder.lock().map(|mut encoder| {
|
|
3088
3248
|
encoder
|
|
@@ -3644,6 +3804,10 @@ mod tests {
|
|
|
3644
3804
|
point_desktop: PointDesktop::Unavailable("layer_shell_unavailable"),
|
|
3645
3805
|
#[cfg(target_os = "linux")]
|
|
3646
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),
|
|
3647
3811
|
capture: Mutex::new(None),
|
|
3648
3812
|
layout: Mutex::new(None),
|
|
3649
3813
|
#[cfg(target_os = "linux")]
|
|
@@ -3723,7 +3887,7 @@ mod tests {
|
|
|
3723
3887
|
)
|
|
3724
3888
|
.await
|
|
3725
3889
|
.expect("a peer connection builds without a network");
|
|
3726
|
-
test_inner_with(events, peer, 64, 64)
|
|
3890
|
+
test_inner_with(events, Arc::new(peer), 64, 64)
|
|
3727
3891
|
}
|
|
3728
3892
|
|
|
3729
3893
|
#[cfg(target_os = "linux")]
|
|
@@ -3858,7 +4022,7 @@ mod tests {
|
|
|
3858
4022
|
|
|
3859
4023
|
fn test_inner_with(
|
|
3860
4024
|
events: tokio_mpsc::UnboundedSender<Notice>,
|
|
3861
|
-
peer: VideoPeer
|
|
4025
|
+
peer: Arc<VideoPeer>,
|
|
3862
4026
|
width: usize,
|
|
3863
4027
|
height: usize,
|
|
3864
4028
|
) -> (Arc<Inner>, Arc<Mutex<Vec<(i16, bool)>>>) {
|
|
@@ -3886,7 +4050,7 @@ mod tests {
|
|
|
3886
4050
|
latest: Arc::new(Mutex::new(None)),
|
|
3887
4051
|
capture_error: Arc::new(Mutex::new(None)),
|
|
3888
4052
|
frame_changes: tokio::sync::watch::channel(0).1,
|
|
3889
|
-
peer: Mutex::new(Some(
|
|
4053
|
+
peer: Mutex::new(Some(peer)),
|
|
3890
4054
|
encoder: Mutex::new(Some(VideoEncoder::Vp9(
|
|
3891
4055
|
Encoder::new(width, height, 1000, 30, 1).expect("an encoder"),
|
|
3892
4056
|
))),
|
|
@@ -3905,6 +4069,10 @@ mod tests {
|
|
|
3905
4069
|
point_desktop: PointDesktop::Unavailable("layer_shell_unavailable"),
|
|
3906
4070
|
#[cfg(target_os = "linux")]
|
|
3907
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),
|
|
3908
4076
|
layout: Mutex::new(Some(Layout::from_environment().expect("a keymap"))),
|
|
3909
4077
|
#[cfg(target_os = "linux")]
|
|
3910
4078
|
audio: Mutex::new(None),
|
|
@@ -4484,7 +4652,7 @@ mod tests {
|
|
|
4484
4652
|
let codec = crate::peer::tests::h264_receiver_codec();
|
|
4485
4653
|
let (peer, _) = crate::peer::tests::answered(vec![codec.clone()]).await;
|
|
4486
4654
|
let (events, mut received) = tokio_mpsc::unbounded_channel();
|
|
4487
|
-
let (inner, _) = test_inner_with(events, peer, 64, 64);
|
|
4655
|
+
let (inner, _) = test_inner_with(events, Arc::new(peer), 64, 64);
|
|
4488
4656
|
let session = Session { inner };
|
|
4489
4657
|
let mut offers = Vec::new();
|
|
4490
4658
|
for _ in 0..3 {
|
|
@@ -4656,6 +4824,72 @@ mod tests {
|
|
|
4656
4824
|
assert_eq!((later.codec, later.encoder), ("vp9", "libvpx"));
|
|
4657
4825
|
}
|
|
4658
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
|
+
|
|
4659
4893
|
#[tokio::test]
|
|
4660
4894
|
async fn a_receiver_that_asks_for_h264_is_sent_h264_from_the_first_frame() {
|
|
4661
4895
|
let Some(info) = tokio::task::spawn_blocking(crate::h264::probe)
|
|
@@ -4668,7 +4902,7 @@ mod tests {
|
|
|
4668
4902
|
let (peer, _answer) =
|
|
4669
4903
|
crate::peer::tests::answered(vec![crate::peer::tests::h264_receiver_codec()]).await;
|
|
4670
4904
|
let (events, _events_rx) = tokio_mpsc::unbounded_channel();
|
|
4671
|
-
let (inner, _recorded) = test_inner_with(events, peer, 320, 240);
|
|
4905
|
+
let (inner, _recorded) = test_inner_with(events, Arc::new(peer), 320, 240);
|
|
4672
4906
|
// The answer is real, so ICE runs and, with no candidates, never
|
|
4673
4907
|
// connects; each state it reports would take back `assume_connected`.
|
|
4674
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;
|
|
101
|
-
since
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
42
|
-
"@desklink/host-darwin-arm64": "0.5.
|
|
41
|
+
"@desklink/host-linux-x64-gnu": "0.5.9",
|
|
42
|
+
"@desklink/host-darwin-arm64": "0.5.9"
|
|
43
43
|
}
|
|
44
44
|
}
|