@desklink/host 0.5.8 → 0.5.10
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 +5 -0
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js +2 -1
- package/dist/bridge.js.map +1 -1
- package/dist/engineProcess.d.ts.map +1 -1
- package/dist/engineProcess.js +1 -0
- package/dist/engineProcess.js.map +1 -1
- package/dist/protocol.d.ts +59 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js.map +1 -1
- package/docs/PROTOCOL.md +92 -5
- package/engine/Cargo.lock +3 -1
- package/engine/Cargo.toml +7 -2
- package/engine/native/agent_overlay_wayland.c +7 -4
- package/engine/src/hyprland.rs +25 -0
- package/engine/src/indicator.rs +29 -12
- package/engine/src/input_mac.rs +433 -0
- package/engine/src/keymap_mac.rs +257 -0
- package/engine/src/mac.rs +8 -0
- package/engine/src/main.rs +111 -13
- package/engine/src/observe.rs +94 -0
- package/engine/src/peer.rs +168 -17
- package/engine/src/portal.rs +83 -1
- package/engine/src/protocol.rs +30 -1
- package/engine/src/session.rs +273 -14
- 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.
|
|
@@ -714,6 +714,8 @@ pub struct OpenRequest {
|
|
|
714
714
|
pub local_frames: bool,
|
|
715
715
|
/// Capture the desktop's own system audio for `session.audio`.
|
|
716
716
|
pub audio: bool,
|
|
717
|
+
/// Listen to the desktop's own pointer buttons, cursor and keys.
|
|
718
|
+
pub observe_input: bool,
|
|
717
719
|
}
|
|
718
720
|
|
|
719
721
|
/// Events a session raises for its consumer.
|
|
@@ -755,6 +757,11 @@ pub enum SessionEvent {
|
|
|
755
757
|
Input {
|
|
756
758
|
input: serde_json::Value,
|
|
757
759
|
},
|
|
760
|
+
/// A pointer button or key the person pressed, observed rather than applied.
|
|
761
|
+
Observed {
|
|
762
|
+
timestamp_us: u64,
|
|
763
|
+
input: crate::protocol::ObservedInput,
|
|
764
|
+
},
|
|
758
765
|
/// The far end needs a fresh reference frame and only the consumer can make
|
|
759
766
|
/// one: the encoder for this track is on its side. Raised by an RTCP PLI or
|
|
760
767
|
/// FIR, and once when the transport connects, because everything sent before
|
|
@@ -762,6 +769,13 @@ pub enum SessionEvent {
|
|
|
762
769
|
KeyframeRequest {
|
|
763
770
|
generation: u64,
|
|
764
771
|
},
|
|
772
|
+
/// An ICE server URL the request asked for that the peer's configuration
|
|
773
|
+
/// does not carry: one the WebRTC stack cannot dial (TURN over anything
|
|
774
|
+
/// but UDP). Reported once per open, naming the URL, so a skipped URL is
|
|
775
|
+
/// a warning the consumer sees rather than a silent drop.
|
|
776
|
+
IceServerSkipped {
|
|
777
|
+
url: String,
|
|
778
|
+
},
|
|
765
779
|
RestoreToken(String),
|
|
766
780
|
CaptureStopped {
|
|
767
781
|
reason: String,
|
|
@@ -821,6 +835,13 @@ struct Inner {
|
|
|
821
835
|
/// closes its Hyprland event socket.
|
|
822
836
|
#[cfg(target_os = "linux")]
|
|
823
837
|
window: Mutex<Option<crate::hyprland::WindowLocator>>,
|
|
838
|
+
/// Which Hyprland output the agent cue helper draws on (a located window
|
|
839
|
+
/// share's desktop mode): the helper respawns only when a press lands
|
|
840
|
+
/// elsewhere. Same for the point helper.
|
|
841
|
+
#[cfg(target_os = "linux")]
|
|
842
|
+
cue_output: Mutex<Option<String>>,
|
|
843
|
+
#[cfg(target_os = "linux")]
|
|
844
|
+
point_output: Mutex<Option<String>>,
|
|
824
845
|
capture: Mutex<Option<FrameSource>>,
|
|
825
846
|
/// Absent for an encoded source: there is no local keyboard to translate a
|
|
826
847
|
/// character through, because the client's keys are forwarded instead.
|
|
@@ -840,6 +861,9 @@ struct Inner {
|
|
|
840
861
|
#[cfg(target_os = "linux")]
|
|
841
862
|
audio: Mutex<Option<crate::audio::AudioCapture>>,
|
|
842
863
|
cursor_info: serde_json::Value,
|
|
864
|
+
/// `session.open`'s `inputObservation` member, fixed when the session opened.
|
|
865
|
+
observation: serde_json::Value,
|
|
866
|
+
observer: Mutex<Option<crate::observe::Tap>>,
|
|
843
867
|
}
|
|
844
868
|
|
|
845
869
|
/// A source whose video arrives already encoded.
|
|
@@ -964,6 +988,7 @@ pub fn capabilities() -> serde_json::Value {
|
|
|
964
988
|
}),
|
|
965
989
|
),
|
|
966
990
|
};
|
|
991
|
+
let observe = crate::input::observe_probe();
|
|
967
992
|
serde_json::json!({
|
|
968
993
|
"protocol": crate::protocol::PROTOCOL_VERSION,
|
|
969
994
|
"engine": format!("desklink-host/{}", env!("CARGO_PKG_VERSION")),
|
|
@@ -995,6 +1020,11 @@ pub fn capabilities() -> serde_json::Value {
|
|
|
995
1020
|
"layout": keymap::LayoutNames::from_environment().identity(),
|
|
996
1021
|
"unavailable_reason": unavailable,
|
|
997
1022
|
"grant": input_grant,
|
|
1023
|
+
"observe": {
|
|
1024
|
+
"mechanism": "cgeventtap",
|
|
1025
|
+
"grant": if observe.is_ok() { "granted" } else { "missing-input-monitoring" },
|
|
1026
|
+
"unavailable_reason": observe.as_ref().err().map(|e| serde_json::json!({"reason": e.reason, "remedy": e.remedy})),
|
|
1027
|
+
},
|
|
998
1028
|
},
|
|
999
1029
|
"clipboard": { "read": true, "write": true, "mime": ["text/plain;charset=utf-8"],
|
|
1000
1030
|
"maxBytes": clipboard::MAX_CLIPBOARD_BYTES },
|
|
@@ -1274,8 +1304,11 @@ impl Session {
|
|
|
1274
1304
|
share_watch.is_some() && source.source_type.as_deref() == Some("Window");
|
|
1275
1305
|
#[cfg(target_os = "linux")]
|
|
1276
1306
|
if request.agent_indicator {
|
|
1307
|
+
// No press has landed yet, so no output is chosen: the
|
|
1308
|
+
// compositor places this first helper, and the first
|
|
1309
|
+
// located press moves it onto the landed output.
|
|
1277
1310
|
indicator =
|
|
1278
|
-
crate::indicator::Indicator::start_wayland(source_w, source_h, desktop)
|
|
1311
|
+
crate::indicator::Indicator::start_wayland(source_w, source_h, desktop, None)
|
|
1279
1312
|
.ok();
|
|
1280
1313
|
}
|
|
1281
1314
|
#[cfg(target_os = "macos")]
|
|
@@ -1463,6 +1496,29 @@ impl Session {
|
|
|
1463
1496
|
"origin": { "x": source.origin_x, "y": source.origin_y },
|
|
1464
1497
|
});
|
|
1465
1498
|
|
|
1499
|
+
// Observation is an addition to the stream, never a condition of it: a
|
|
1500
|
+
// desktop that cannot be observed still streams, and the reply says why.
|
|
1501
|
+
let observed = request.observe_input.then(|| {
|
|
1502
|
+
let sink: crate::observe::Sink = {
|
|
1503
|
+
let events = events.clone();
|
|
1504
|
+
let id = id.clone();
|
|
1505
|
+
Arc::new(move |observation| {
|
|
1506
|
+
let event = match observation {
|
|
1507
|
+
crate::observe::Observation::Input { timestamp_us, input } => {
|
|
1508
|
+
SessionEvent::Observed { timestamp_us, input }
|
|
1509
|
+
}
|
|
1510
|
+
crate::observe::Observation::Cursor(position) => SessionEvent::Cursor { position },
|
|
1511
|
+
};
|
|
1512
|
+
let _ = events.send(Notice { session_id: id.clone(), event });
|
|
1513
|
+
})
|
|
1514
|
+
};
|
|
1515
|
+
crate::observe::start(source.node_id, (source_w, source_h), sink)
|
|
1516
|
+
});
|
|
1517
|
+
if let Some(Err(error)) = &observed {
|
|
1518
|
+
log::warn!("input observation unavailable: {} ({})", error.reason, error.code);
|
|
1519
|
+
}
|
|
1520
|
+
let observation = crate::observe::status(observed.as_ref());
|
|
1521
|
+
|
|
1466
1522
|
let pipeline = Arc::new(AtomicBool::new(true));
|
|
1467
1523
|
let inner = Arc::new(Inner {
|
|
1468
1524
|
id,
|
|
@@ -1493,6 +1549,10 @@ impl Session {
|
|
|
1493
1549
|
},
|
|
1494
1550
|
#[cfg(target_os = "linux")]
|
|
1495
1551
|
window: Mutex::new(window),
|
|
1552
|
+
#[cfg(target_os = "linux")]
|
|
1553
|
+
cue_output: Mutex::new(None),
|
|
1554
|
+
#[cfg(target_os = "linux")]
|
|
1555
|
+
point_output: Mutex::new(None),
|
|
1496
1556
|
capture: Mutex::new(Some(capture)),
|
|
1497
1557
|
layout: Mutex::new(Some(layout)),
|
|
1498
1558
|
#[cfg(target_os = "linux")]
|
|
@@ -1505,6 +1565,8 @@ impl Session {
|
|
|
1505
1565
|
encoded: Mutex::new(None),
|
|
1506
1566
|
local_frames: request.local_frames,
|
|
1507
1567
|
cursor_info,
|
|
1568
|
+
observation,
|
|
1569
|
+
observer: Mutex::new(observed.and_then(Result::ok)),
|
|
1508
1570
|
});
|
|
1509
1571
|
|
|
1510
1572
|
inner.notify(SessionEvent::Description {
|
|
@@ -1593,6 +1655,10 @@ impl Session {
|
|
|
1593
1655
|
point_desktop: PointDesktop::Unavailable("layer_shell_unavailable"),
|
|
1594
1656
|
#[cfg(target_os = "linux")]
|
|
1595
1657
|
window: Mutex::new(None),
|
|
1658
|
+
#[cfg(target_os = "linux")]
|
|
1659
|
+
cue_output: Mutex::new(None),
|
|
1660
|
+
#[cfg(target_os = "linux")]
|
|
1661
|
+
point_output: Mutex::new(None),
|
|
1596
1662
|
capture: Mutex::new(None),
|
|
1597
1663
|
layout: Mutex::new(None),
|
|
1598
1664
|
#[cfg(target_os = "linux")]
|
|
@@ -1616,6 +1682,13 @@ impl Session {
|
|
|
1616
1682
|
})),
|
|
1617
1683
|
local_frames: false,
|
|
1618
1684
|
cursor_info: serde_json::json!({"mode":"unavailable", "positions":false}),
|
|
1685
|
+
observation: serde_json::json!({
|
|
1686
|
+
"observing": false,
|
|
1687
|
+
"code": "unsupported-source",
|
|
1688
|
+
"reason": "consumer-encoded video has no desktop of its own to observe",
|
|
1689
|
+
"remedy": "observe the device where its pixels come from",
|
|
1690
|
+
}),
|
|
1691
|
+
observer: Mutex::new(None),
|
|
1619
1692
|
});
|
|
1620
1693
|
inner.notify(SessionEvent::State {
|
|
1621
1694
|
capture: "consented",
|
|
@@ -1681,6 +1754,10 @@ impl Session {
|
|
|
1681
1754
|
&self.inner.cursor_info
|
|
1682
1755
|
}
|
|
1683
1756
|
|
|
1757
|
+
pub fn observation_info(&self) -> &serde_json::Value {
|
|
1758
|
+
&self.inner.observation
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1684
1761
|
pub fn geometry(&self) -> &serde_json::Value {
|
|
1685
1762
|
&self.inner.geometry
|
|
1686
1763
|
}
|
|
@@ -1739,6 +1816,10 @@ impl Session {
|
|
|
1739
1816
|
if let Some(helper) = overlay.take() {
|
|
1740
1817
|
helper.stop();
|
|
1741
1818
|
}
|
|
1819
|
+
#[cfg(target_os = "linux")]
|
|
1820
|
+
{
|
|
1821
|
+
*lock(&self.inner.point_output) = None;
|
|
1822
|
+
}
|
|
1742
1823
|
return Ok(());
|
|
1743
1824
|
}
|
|
1744
1825
|
let (x, y) = params
|
|
@@ -1763,21 +1844,35 @@ impl Session {
|
|
|
1763
1844
|
));
|
|
1764
1845
|
}
|
|
1765
1846
|
// A located window's point shows where a press there would land, on
|
|
1766
|
-
// the output holding it; the
|
|
1767
|
-
//
|
|
1847
|
+
// the output holding it; the helper moves only when that output
|
|
1848
|
+
// changes, so repeats on one output reuse it with no new process.
|
|
1768
1849
|
#[cfg(target_os = "linux")]
|
|
1769
1850
|
let landed = match lock(&self.inner.window).as_ref() {
|
|
1770
1851
|
Some(window) => {
|
|
1771
1852
|
let (px, py) = window
|
|
1772
1853
|
.locate(x, y)
|
|
1773
1854
|
.map_err(|message| SessionError::new("indicator-unavailable", message))?;
|
|
1774
|
-
if let Some(helper) = overlay.take() {
|
|
1775
|
-
helper.stop();
|
|
1776
|
-
}
|
|
1777
1855
|
Some((px.floor() as i64, py.floor() as i64))
|
|
1778
1856
|
}
|
|
1779
1857
|
None => None,
|
|
1780
1858
|
};
|
|
1859
|
+
#[cfg(target_os = "linux")]
|
|
1860
|
+
let landed_output: Option<String> = match landed {
|
|
1861
|
+
Some((px, py)) => {
|
|
1862
|
+
crate::hyprland::monitor_containing(px as f64, py as f64).unwrap_or(None)
|
|
1863
|
+
}
|
|
1864
|
+
None => None,
|
|
1865
|
+
};
|
|
1866
|
+
#[cfg(target_os = "linux")]
|
|
1867
|
+
if match (&landed, &landed_output) {
|
|
1868
|
+
(Some(_), output) => lock(&self.inner.point_output).clone() != *output,
|
|
1869
|
+
(None, _) => overlay.is_none(),
|
|
1870
|
+
} {
|
|
1871
|
+
if let Some(helper) = overlay.take() {
|
|
1872
|
+
helper.stop();
|
|
1873
|
+
}
|
|
1874
|
+
*lock(&self.inner.point_output) = landed_output.clone();
|
|
1875
|
+
}
|
|
1781
1876
|
#[cfg(not(target_os = "windows"))]
|
|
1782
1877
|
if overlay.is_none() {
|
|
1783
1878
|
#[cfg(target_os = "linux")]
|
|
@@ -2133,12 +2228,19 @@ impl Inner {
|
|
|
2133
2228
|
// handlers, and `shutdown` exits right after this. The encode thread
|
|
2134
2229
|
// finds it gone and stops.
|
|
2135
2230
|
drop(lock(&self.encoder).take());
|
|
2231
|
+
// Stop listening to the person's keys the moment the session ends.
|
|
2232
|
+
drop(lock(&self.observer).take());
|
|
2136
2233
|
if let Ok(mut indicator) = self.indicator.lock() {
|
|
2137
2234
|
indicator.take();
|
|
2138
2235
|
}
|
|
2139
2236
|
if let Some(helper) = lock(&self.point_overlay).take() {
|
|
2140
2237
|
helper.stop();
|
|
2141
2238
|
}
|
|
2239
|
+
#[cfg(target_os = "linux")]
|
|
2240
|
+
{
|
|
2241
|
+
*lock(&self.cue_output) = None;
|
|
2242
|
+
*lock(&self.point_output) = None;
|
|
2243
|
+
}
|
|
2142
2244
|
// Release first, then tear down: a stuck modifier is the one failure the
|
|
2143
2245
|
// user cannot undo by reconnecting.
|
|
2144
2246
|
if let Ok(mut input) = self.input.lock() {
|
|
@@ -2318,6 +2420,10 @@ impl Inner {
|
|
|
2318
2420
|
self.reject(seq, "input-unavailable", "no input backend");
|
|
2319
2421
|
return;
|
|
2320
2422
|
}
|
|
2423
|
+
// Only a located window-share press can move the cue helper: other
|
|
2424
|
+
// paths have no desktop point to choose an output from.
|
|
2425
|
+
#[cfg(target_os = "linux")]
|
|
2426
|
+
let located_press = located && matches!(&message, ControlMessage::Pointer { .. });
|
|
2321
2427
|
let outcome = match message {
|
|
2322
2428
|
ControlMessage::Pointer {
|
|
2323
2429
|
phase,
|
|
@@ -2361,6 +2467,14 @@ impl Inner {
|
|
|
2361
2467
|
};
|
|
2362
2468
|
match outcome {
|
|
2363
2469
|
Ok(()) => {
|
|
2470
|
+
#[cfg(target_os = "linux")]
|
|
2471
|
+
if located_press {
|
|
2472
|
+
// Onto the landed output before the cue is told where to
|
|
2473
|
+
// draw, so a cross-output tap draws there, not nowhere.
|
|
2474
|
+
if let Some((_, x, y)) = feedback {
|
|
2475
|
+
self.move_cue_to(x, y);
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2364
2478
|
if let Some((kind, x, y)) = feedback {
|
|
2365
2479
|
if let Ok(mut indicator) = self.indicator.lock() {
|
|
2366
2480
|
if let Some(indicator) = indicator.as_mut() {
|
|
@@ -2426,6 +2540,49 @@ impl Inner {
|
|
|
2426
2540
|
}
|
|
2427
2541
|
}
|
|
2428
2542
|
|
|
2543
|
+
/// Move the agent cue helper onto the output holding desktop point
|
|
2544
|
+
/// `(x, y)`, where a located press just landed. A press on the helper's
|
|
2545
|
+
/// own output reuses it with no new process; anything else respawns it
|
|
2546
|
+
/// with the landed origin, which selects that output through the point
|
|
2547
|
+
/// helper's xdg-output selection. Best-effort: when the output or the
|
|
2548
|
+
/// respawn is unavailable, the standing helper keeps the cue.
|
|
2549
|
+
#[cfg(target_os = "linux")]
|
|
2550
|
+
fn move_cue_to(&self, x: i64, y: i64) {
|
|
2551
|
+
let output = match crate::hyprland::monitor_containing(x as f64, y as f64) {
|
|
2552
|
+
Ok(output) => output,
|
|
2553
|
+
Err(error) => {
|
|
2554
|
+
log::warn!("agent cue stays on its output: {error}");
|
|
2555
|
+
return;
|
|
2556
|
+
}
|
|
2557
|
+
};
|
|
2558
|
+
if lock(&self.cue_output).clone() == output {
|
|
2559
|
+
return;
|
|
2560
|
+
}
|
|
2561
|
+
let Some(name) = output else { return };
|
|
2562
|
+
let mut indicator = lock(&self.indicator);
|
|
2563
|
+
if let Some(helper) = indicator.take() {
|
|
2564
|
+
helper.stop();
|
|
2565
|
+
}
|
|
2566
|
+
let (width, height) = (
|
|
2567
|
+
self.source.width.max(1) as usize,
|
|
2568
|
+
self.source.height.max(1) as usize,
|
|
2569
|
+
);
|
|
2570
|
+
match crate::indicator::Indicator::start_wayland(
|
|
2571
|
+
width,
|
|
2572
|
+
height,
|
|
2573
|
+
true,
|
|
2574
|
+
Some((x as i32, y as i32)),
|
|
2575
|
+
) {
|
|
2576
|
+
Ok(helper) => {
|
|
2577
|
+
*indicator = Some(helper);
|
|
2578
|
+
*lock(&self.cue_output) = Some(name);
|
|
2579
|
+
}
|
|
2580
|
+
Err(error) => {
|
|
2581
|
+
log::warn!("agent cue stays on its output: the {name} helper did not start: {error:#}");
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2429
2586
|
fn pointer(
|
|
2430
2587
|
&self,
|
|
2431
2588
|
phase: PointerPhase,
|
|
@@ -2804,6 +2961,12 @@ fn auto_bitrate(width: usize, height: usize) -> u32 {
|
|
|
2804
2961
|
const QUEUE_HIGH: Duration = Duration::from_millis(25);
|
|
2805
2962
|
/// Below this the path has room again and the target may creep back up.
|
|
2806
2963
|
const QUEUE_LOW: Duration = Duration::from_millis(10);
|
|
2964
|
+
/// The same thresholds for a TCP path. TCP never drops, so its queue is read
|
|
2965
|
+
/// as delay alone, and a relay's bridge adds 20-30 ms of delivery jitter that is
|
|
2966
|
+
/// not queueing: at 25/10 ms that jitter backs the target off to its floor
|
|
2967
|
+
/// with the link idle. Only a queue that stands over 100 ms is a backlog here.
|
|
2968
|
+
const QUEUE_HIGH_TCP: Duration = Duration::from_millis(100);
|
|
2969
|
+
const QUEUE_LOW_TCP: Duration = Duration::from_millis(60);
|
|
2807
2970
|
/// How often queueing delay may back the target off: long enough for the last
|
|
2808
2971
|
/// back-off to reach the far end and show in its feedback, short enough that a
|
|
2809
2972
|
/// queue is drained in a few steps rather than a few seconds.
|
|
@@ -2825,6 +2988,8 @@ struct RateControl {
|
|
|
2825
2988
|
last_change: Option<Instant>,
|
|
2826
2989
|
/// The last queueing delay reported, while the path reports one.
|
|
2827
2990
|
queue: Option<Duration>,
|
|
2991
|
+
queue_high: Duration,
|
|
2992
|
+
queue_low: Duration,
|
|
2828
2993
|
}
|
|
2829
2994
|
|
|
2830
2995
|
impl RateControl {
|
|
@@ -2843,9 +3008,21 @@ impl RateControl {
|
|
|
2843
3008
|
target: ceiling,
|
|
2844
3009
|
last_change: None,
|
|
2845
3010
|
queue: None,
|
|
3011
|
+
queue_high: QUEUE_HIGH,
|
|
3012
|
+
queue_low: QUEUE_LOW,
|
|
2846
3013
|
}
|
|
2847
3014
|
}
|
|
2848
3015
|
|
|
3016
|
+
/// Read queueing delay against the thresholds for the transport the selected
|
|
3017
|
+
/// pair runs over.
|
|
3018
|
+
fn set_tcp(&mut self, tcp: bool) {
|
|
3019
|
+
(self.queue_high, self.queue_low) = if tcp {
|
|
3020
|
+
(QUEUE_HIGH_TCP, QUEUE_LOW_TCP)
|
|
3021
|
+
} else {
|
|
3022
|
+
(QUEUE_HIGH, QUEUE_LOW)
|
|
3023
|
+
};
|
|
3024
|
+
}
|
|
3025
|
+
|
|
2849
3026
|
/// The new target when these loss reports (RFC 3550 fractions, /256) or
|
|
2850
3027
|
/// this path report move it: down on a standing queue at most every
|
|
2851
3028
|
/// `DELAY_BACKOFF_EVERY`, otherwise at most once a second.
|
|
@@ -2856,7 +3033,7 @@ impl RateControl {
|
|
|
2856
3033
|
let since = self.last_change.map(|at| now.duration_since(at));
|
|
2857
3034
|
let waited = |gap: Duration| since.is_none_or(|since| since >= gap);
|
|
2858
3035
|
let floor = (self.ceiling / 8).max(500);
|
|
2859
|
-
let next = if let Some(path) = path.filter(|path| path.queue_delay >
|
|
3036
|
+
let next = if let Some(path) = path.filter(|path| path.queue_delay > self.queue_high) {
|
|
2860
3037
|
if !waited(DELAY_BACKOFF_EVERY) {
|
|
2861
3038
|
return None;
|
|
2862
3039
|
}
|
|
@@ -2868,7 +3045,7 @@ impl RateControl {
|
|
|
2868
3045
|
return None;
|
|
2869
3046
|
}
|
|
2870
3047
|
let loss = worst as f64 / 256.0;
|
|
2871
|
-
let roomy = self.queue.is_none_or(|queue| queue <
|
|
3048
|
+
let roomy = self.queue.is_none_or(|queue| queue < self.queue_low);
|
|
2872
3049
|
if loss > 0.10 {
|
|
2873
3050
|
((self.target as f64 * 0.7) as u32).max(floor)
|
|
2874
3051
|
} else if loss < 0.02 && roomy {
|
|
@@ -3117,6 +3294,9 @@ fn spawn_pipeline(
|
|
|
3117
3294
|
}
|
|
3118
3295
|
let reports = peer.take_loss_reports();
|
|
3119
3296
|
let path = peer.take_path_report();
|
|
3297
|
+
if path.is_some() {
|
|
3298
|
+
rate.set_tcp(handle.block_on(peer.selected_pair_is_tcp()));
|
|
3299
|
+
}
|
|
3120
3300
|
if let Some(kbps) = rate.update(&reports, path, Instant::now()) {
|
|
3121
3301
|
let applied = inner.encoder.lock().map(|mut encoder| {
|
|
3122
3302
|
encoder
|
|
@@ -3678,6 +3858,10 @@ mod tests {
|
|
|
3678
3858
|
point_desktop: PointDesktop::Unavailable("layer_shell_unavailable"),
|
|
3679
3859
|
#[cfg(target_os = "linux")]
|
|
3680
3860
|
window: Mutex::new(None),
|
|
3861
|
+
#[cfg(target_os = "linux")]
|
|
3862
|
+
cue_output: Mutex::new(None),
|
|
3863
|
+
#[cfg(target_os = "linux")]
|
|
3864
|
+
point_output: Mutex::new(None),
|
|
3681
3865
|
capture: Mutex::new(None),
|
|
3682
3866
|
layout: Mutex::new(None),
|
|
3683
3867
|
#[cfg(target_os = "linux")]
|
|
@@ -3699,6 +3883,8 @@ mod tests {
|
|
|
3699
3883
|
})),
|
|
3700
3884
|
local_frames: false,
|
|
3701
3885
|
cursor_info: serde_json::json!({"mode":"unavailable", "positions":false}),
|
|
3886
|
+
observation: serde_json::json!({"observing": false}),
|
|
3887
|
+
observer: Mutex::new(None),
|
|
3702
3888
|
});
|
|
3703
3889
|
spawn_peer_events(&inner, peer_events_rx);
|
|
3704
3890
|
spawn_encoded(&inner, feed_rx);
|
|
@@ -3757,7 +3943,7 @@ mod tests {
|
|
|
3757
3943
|
)
|
|
3758
3944
|
.await
|
|
3759
3945
|
.expect("a peer connection builds without a network");
|
|
3760
|
-
test_inner_with(events, peer, 64, 64)
|
|
3946
|
+
test_inner_with(events, Arc::new(peer), 64, 64)
|
|
3761
3947
|
}
|
|
3762
3948
|
|
|
3763
3949
|
#[cfg(target_os = "linux")]
|
|
@@ -3892,7 +4078,7 @@ mod tests {
|
|
|
3892
4078
|
|
|
3893
4079
|
fn test_inner_with(
|
|
3894
4080
|
events: tokio_mpsc::UnboundedSender<Notice>,
|
|
3895
|
-
peer: VideoPeer
|
|
4081
|
+
peer: Arc<VideoPeer>,
|
|
3896
4082
|
width: usize,
|
|
3897
4083
|
height: usize,
|
|
3898
4084
|
) -> (Arc<Inner>, Arc<Mutex<Vec<(i16, bool)>>>) {
|
|
@@ -3920,7 +4106,7 @@ mod tests {
|
|
|
3920
4106
|
latest: Arc::new(Mutex::new(None)),
|
|
3921
4107
|
capture_error: Arc::new(Mutex::new(None)),
|
|
3922
4108
|
frame_changes: tokio::sync::watch::channel(0).1,
|
|
3923
|
-
peer: Mutex::new(Some(
|
|
4109
|
+
peer: Mutex::new(Some(peer)),
|
|
3924
4110
|
encoder: Mutex::new(Some(VideoEncoder::Vp9(
|
|
3925
4111
|
Encoder::new(width, height, 1000, 30, 1).expect("an encoder"),
|
|
3926
4112
|
))),
|
|
@@ -3939,6 +4125,10 @@ mod tests {
|
|
|
3939
4125
|
point_desktop: PointDesktop::Unavailable("layer_shell_unavailable"),
|
|
3940
4126
|
#[cfg(target_os = "linux")]
|
|
3941
4127
|
window: Mutex::new(None),
|
|
4128
|
+
#[cfg(target_os = "linux")]
|
|
4129
|
+
cue_output: Mutex::new(None),
|
|
4130
|
+
#[cfg(target_os = "linux")]
|
|
4131
|
+
point_output: Mutex::new(None),
|
|
3942
4132
|
layout: Mutex::new(Some(Layout::from_environment().expect("a keymap"))),
|
|
3943
4133
|
#[cfg(target_os = "linux")]
|
|
3944
4134
|
audio: Mutex::new(None),
|
|
@@ -3950,6 +4140,8 @@ mod tests {
|
|
|
3950
4140
|
encoded: Mutex::new(None),
|
|
3951
4141
|
local_frames: true,
|
|
3952
4142
|
cursor_info: serde_json::json!({"mode":"embedded", "positions":false}),
|
|
4143
|
+
observation: serde_json::json!({"observing": false}),
|
|
4144
|
+
observer: Mutex::new(None),
|
|
3953
4145
|
});
|
|
3954
4146
|
(inner, recorded)
|
|
3955
4147
|
}
|
|
@@ -4350,6 +4542,7 @@ mod tests {
|
|
|
4350
4542
|
agent_indicator: false,
|
|
4351
4543
|
local_frames: true,
|
|
4352
4544
|
audio: false,
|
|
4545
|
+
observe_input: false,
|
|
4353
4546
|
}
|
|
4354
4547
|
}
|
|
4355
4548
|
|
|
@@ -4518,7 +4711,7 @@ mod tests {
|
|
|
4518
4711
|
let codec = crate::peer::tests::h264_receiver_codec();
|
|
4519
4712
|
let (peer, _) = crate::peer::tests::answered(vec![codec.clone()]).await;
|
|
4520
4713
|
let (events, mut received) = tokio_mpsc::unbounded_channel();
|
|
4521
|
-
let (inner, _) = test_inner_with(events, peer, 64, 64);
|
|
4714
|
+
let (inner, _) = test_inner_with(events, Arc::new(peer), 64, 64);
|
|
4522
4715
|
let session = Session { inner };
|
|
4523
4716
|
let mut offers = Vec::new();
|
|
4524
4717
|
for _ in 0..3 {
|
|
@@ -4690,6 +4883,72 @@ mod tests {
|
|
|
4690
4883
|
assert_eq!((later.codec, later.encoder), ("vp9", "libvpx"));
|
|
4691
4884
|
}
|
|
4692
4885
|
|
|
4886
|
+
/// The rate target after 3 s of delivery jitter on a connected pair: a queue
|
|
4887
|
+
/// that alternates between 5 and 30 ms with nothing lost, and 800 kbps
|
|
4888
|
+
/// delivered. The pipeline runs for real; only the path reports are fed.
|
|
4889
|
+
async fn target_under_bridge_jitter(tcp: bool) -> u32 {
|
|
4890
|
+
let peers = crate::peer::tests::connected(
|
|
4891
|
+
if tcp {
|
|
4892
|
+
Vec::new()
|
|
4893
|
+
} else {
|
|
4894
|
+
vec![std::net::SocketAddr::from(([0, 0, 0, 0], 0))]
|
|
4895
|
+
},
|
|
4896
|
+
if tcp {
|
|
4897
|
+
vec![std::net::SocketAddr::from(([127, 0, 0, 1], 0))]
|
|
4898
|
+
} else {
|
|
4899
|
+
Vec::new()
|
|
4900
|
+
},
|
|
4901
|
+
tcp,
|
|
4902
|
+
)
|
|
4903
|
+
.await;
|
|
4904
|
+
let engine = Arc::clone(&peers.engine);
|
|
4905
|
+
assert_eq!(
|
|
4906
|
+
engine.selected_pair_is_tcp().await,
|
|
4907
|
+
tcp,
|
|
4908
|
+
"the selected pair must run over {}",
|
|
4909
|
+
if tcp { "TCP" } else { "UDP" }
|
|
4910
|
+
);
|
|
4911
|
+
let (events, _events_rx) = tokio_mpsc::unbounded_channel();
|
|
4912
|
+
let (inner, _recorded) = test_inner_with(events, Arc::clone(&engine), 64, 64);
|
|
4913
|
+
let (_frame_tx, frame_rx) = latest_frame();
|
|
4914
|
+
spawn_pipeline(
|
|
4915
|
+
&inner,
|
|
4916
|
+
Arc::clone(&engine),
|
|
4917
|
+
frame_rx,
|
|
4918
|
+
Arc::new(AtomicBool::new(true)),
|
|
4919
|
+
30,
|
|
4920
|
+
1000,
|
|
4921
|
+
);
|
|
4922
|
+
for step in 0..300 {
|
|
4923
|
+
let queue_delay = Duration::from_millis(if step % 2 == 0 { 5 } else { 30 });
|
|
4924
|
+
engine.report_path(PathReport {
|
|
4925
|
+
queue_delay,
|
|
4926
|
+
delivered_kbps: 800,
|
|
4927
|
+
});
|
|
4928
|
+
tokio::time::sleep(Duration::from_millis(10)).await;
|
|
4929
|
+
}
|
|
4930
|
+
let target = inner.metrics.lock().unwrap().target_kbps;
|
|
4931
|
+
drop(peers);
|
|
4932
|
+
target
|
|
4933
|
+
}
|
|
4934
|
+
|
|
4935
|
+
#[tokio::test(flavor = "multi_thread")]
|
|
4936
|
+
async fn a_tcp_path_with_bridge_jitter_holds_its_rate_ceiling() {
|
|
4937
|
+
assert_eq!(
|
|
4938
|
+
target_under_bridge_jitter(true).await,
|
|
4939
|
+
1000,
|
|
4940
|
+
"a 5-30 ms jitter that is not a queue must not back a TCP path off"
|
|
4941
|
+
);
|
|
4942
|
+
}
|
|
4943
|
+
|
|
4944
|
+
#[tokio::test(flavor = "multi_thread")]
|
|
4945
|
+
async fn a_udp_path_with_the_same_jitter_still_backs_off() {
|
|
4946
|
+
assert!(
|
|
4947
|
+
target_under_bridge_jitter(false).await < 1000,
|
|
4948
|
+
"a UDP path reads the same 30 ms as a standing queue and must back off"
|
|
4949
|
+
);
|
|
4950
|
+
}
|
|
4951
|
+
|
|
4693
4952
|
#[tokio::test]
|
|
4694
4953
|
async fn a_receiver_that_asks_for_h264_is_sent_h264_from_the_first_frame() {
|
|
4695
4954
|
let Some(info) = tokio::task::spawn_blocking(crate::h264::probe)
|
|
@@ -4702,7 +4961,7 @@ mod tests {
|
|
|
4702
4961
|
let (peer, _answer) =
|
|
4703
4962
|
crate::peer::tests::answered(vec![crate::peer::tests::h264_receiver_codec()]).await;
|
|
4704
4963
|
let (events, _events_rx) = tokio_mpsc::unbounded_channel();
|
|
4705
|
-
let (inner, _recorded) = test_inner_with(events, peer, 320, 240);
|
|
4964
|
+
let (inner, _recorded) = test_inner_with(events, Arc::new(peer), 320, 240);
|
|
4706
4965
|
// The answer is real, so ICE runs and, with no candidates, never
|
|
4707
4966
|
// connects; each state it reports would take back `assume_connected`.
|
|
4708
4967
|
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.10",
|
|
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.10",
|
|
42
|
+
"@desklink/host-darwin-arm64": "0.5.10"
|
|
43
43
|
}
|
|
44
44
|
}
|