@desklink/host 0.5.3 → 0.5.6
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 +33 -19
- package/dist/bridge.js +3 -3
- package/dist/bridge.js.map +1 -1
- package/dist/engineProcess.d.ts +5 -0
- package/dist/engineProcess.d.ts.map +1 -1
- package/dist/engineProcess.js +6 -3
- package/dist/engineProcess.js.map +1 -1
- package/dist/protocol.d.ts +39 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js.map +1 -1
- package/docs/BOUNDARY.md +4 -2
- package/docs/PROTOCOL.md +70 -7
- package/engine/Cargo.lock +6 -8
- package/engine/Cargo.toml +13 -1
- package/engine/src/audio.rs +398 -0
- package/engine/src/clipboard.rs +0 -34
- package/engine/src/convert.rs +0 -170
- package/engine/src/cursor.rs +0 -23
- package/engine/src/input.rs +0 -65
- package/engine/src/input_mac.rs +0 -11
- package/engine/src/keeper.rs +0 -137
- package/engine/src/keymap.rs +0 -107
- package/engine/src/keymap_mac.rs +0 -25
- package/engine/src/mac.rs +1 -28
- package/engine/src/main.rs +22 -0
- package/engine/src/peer.rs +297 -0
- package/engine/src/portal.rs +0 -18
- package/engine/src/protocol.rs +22 -162
- package/engine/src/session.rs +176 -3
- package/engine/src/win.rs +1 -0
- package/engine/src/x11.rs +0 -13
- package/engine/vendor/rtc-ice/Cargo.toml +137 -0
- package/engine/vendor/rtc-ice/DESKLINK-PATCH.md +111 -0
- package/engine/vendor/rtc-ice/LICENSE-APACHE +201 -0
- package/engine/vendor/rtc-ice/LICENSE-MIT +21 -0
- package/engine/vendor/rtc-ice/README.md +30 -0
- package/engine/vendor/rtc-ice/examples/ping_pong.rs +376 -0
- package/engine/vendor/rtc-ice/src/agent/agent_config.rs +126 -0
- package/engine/vendor/rtc-ice/src/agent/agent_proto.rs +172 -0
- package/engine/vendor/rtc-ice/src/agent/agent_selector.rs +647 -0
- package/engine/vendor/rtc-ice/src/agent/agent_stats.rs +292 -0
- package/engine/vendor/rtc-ice/src/agent/agent_test.rs +3594 -0
- package/engine/vendor/rtc-ice/src/agent/mod.rs +1753 -0
- package/engine/vendor/rtc-ice/src/attributes/control/control_test.rs +168 -0
- package/engine/vendor/rtc-ice/src/attributes/control/mod.rs +142 -0
- package/engine/vendor/rtc-ice/src/attributes/mod.rs +6 -0
- package/engine/vendor/rtc-ice/src/attributes/priority/mod.rs +37 -0
- package/engine/vendor/rtc-ice/src/attributes/priority/priority_test.rs +39 -0
- package/engine/vendor/rtc-ice/src/attributes/use_candidate/mod.rs +33 -0
- package/engine/vendor/rtc-ice/src/attributes/use_candidate/use_candidate_test.rs +19 -0
- package/engine/vendor/rtc-ice/src/candidate/candidate_host.rs +53 -0
- package/engine/vendor/rtc-ice/src/candidate/candidate_pair.rs +215 -0
- package/engine/vendor/rtc-ice/src/candidate/candidate_pair_test.rs +201 -0
- package/engine/vendor/rtc-ice/src/candidate/candidate_peer_reflexive.rs +49 -0
- package/engine/vendor/rtc-ice/src/candidate/candidate_relay.rs +53 -0
- package/engine/vendor/rtc-ice/src/candidate/candidate_relay_test.rs +114 -0
- package/engine/vendor/rtc-ice/src/candidate/candidate_server_reflexive.rs +53 -0
- package/engine/vendor/rtc-ice/src/candidate/candidate_server_reflexive_test.rs +91 -0
- package/engine/vendor/rtc-ice/src/candidate/candidate_test.rs +540 -0
- package/engine/vendor/rtc-ice/src/candidate/mod.rs +679 -0
- package/engine/vendor/rtc-ice/src/dl_trace.rs +49 -0
- package/engine/vendor/rtc-ice/src/lib.rs +88 -0
- package/engine/vendor/rtc-ice/src/mdns/mdns_test.rs +150 -0
- package/engine/vendor/rtc-ice/src/mdns/mod.rs +69 -0
- package/engine/vendor/rtc-ice/src/network_type/mod.rs +162 -0
- package/engine/vendor/rtc-ice/src/network_type/network_type_test.rs +95 -0
- package/engine/vendor/rtc-ice/src/rand/mod.rs +91 -0
- package/engine/vendor/rtc-ice/src/rand/rand_test.rs +115 -0
- package/engine/vendor/rtc-ice/src/state/mod.rs +106 -0
- package/engine/vendor/rtc-ice/src/state/state_test.rs +46 -0
- package/engine/vendor/rtc-ice/src/stats/mod.rs +166 -0
- package/engine/vendor/rtc-ice/src/tcp_type/mod.rs +47 -0
- package/engine/vendor/rtc-ice/src/tcp_type/tcp_type_test.rs +18 -0
- package/engine/vendor/rtc-ice/src/url/mod.rs +266 -0
- package/engine/vendor/rtc-ice/src/url/url_test.rs +142 -0
- package/package.json +8 -3
package/engine/src/peer.rs
CHANGED
|
@@ -47,6 +47,33 @@ 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.
|
|
51
|
+
// Enabled with `DESKLINK_ICE_TRACE=1`; stamps microseconds since the Unix epoch
|
|
52
|
+
// so these lines interleave with the vendored ICE agent's `[ice ...]` lines in
|
|
53
|
+
// one order. Droppable: grep `desklink scout trace`.
|
|
54
|
+
pub(crate) fn dl_trace_on() -> bool {
|
|
55
|
+
std::env::var("DESKLINK_ICE_TRACE")
|
|
56
|
+
.map(|v| v != "0" && !v.is_empty())
|
|
57
|
+
.unwrap_or(false)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
pub(crate) fn dl_trace_us() -> u128 {
|
|
61
|
+
std::time::SystemTime::now()
|
|
62
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
63
|
+
.map(|d| d.as_micros())
|
|
64
|
+
.unwrap_or(0)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
macro_rules! dl_eng {
|
|
68
|
+
($($arg:tt)*) => {
|
|
69
|
+
if $crate::peer::dl_trace_on() {
|
|
70
|
+
eprintln!("[eng {}] {}", $crate::peer::dl_trace_us(), format_args!($($arg)*));
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
pub(crate) use dl_eng;
|
|
76
|
+
|
|
50
77
|
/// VP9 clock rate, fixed by RFC 9628.
|
|
51
78
|
const VP9_CLOCK_RATE: u32 = 90_000;
|
|
52
79
|
|
|
@@ -420,6 +447,12 @@ impl PeerConnectionEventHandler for Handler {
|
|
|
420
447
|
}
|
|
421
448
|
|
|
422
449
|
async fn on_connection_state_change(&self, state: RTCPeerConnectionState) {
|
|
450
|
+
// desklink scout trace (dl-roam-postnom-attr): the engine's rtc-level
|
|
451
|
+
// transport state event, on the same microsecond clock as the vendored
|
|
452
|
+
// agent's `[ice ...]` lines, so the rtc Connected gate is separable.
|
|
453
|
+
if dl_trace_on() {
|
|
454
|
+
dl_eng!("peer_state_change {state:?}");
|
|
455
|
+
}
|
|
423
456
|
let connected = matches!(state, RTCPeerConnectionState::Connected);
|
|
424
457
|
if connected {
|
|
425
458
|
self.wants_keyframe.store(true, Ordering::SeqCst);
|
|
@@ -1153,6 +1186,7 @@ fn lock<T>(mutex: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
|
|
|
1153
1186
|
#[cfg(test)]
|
|
1154
1187
|
pub(crate) mod tests {
|
|
1155
1188
|
use super::*;
|
|
1189
|
+
use webrtc::media_stream::track_remote::{TrackRemote, TrackRemoteEvent};
|
|
1156
1190
|
|
|
1157
1191
|
#[test]
|
|
1158
1192
|
fn a_standing_queue_is_measured_and_one_frames_burst_is_not() {
|
|
@@ -1589,6 +1623,269 @@ pub(crate) mod tests {
|
|
|
1589
1623
|
.expect("the restart answer is accepted");
|
|
1590
1624
|
}
|
|
1591
1625
|
|
|
1626
|
+
/// How long the restarted generation may take to nominate and reconnect,
|
|
1627
|
+
/// measured from the answered offer: two `check_interval` ticks. The vendored
|
|
1628
|
+
/// agent ticks every 200 ms (`vendor/rtc-ice/src/agent/agent_config.rs`), and
|
|
1629
|
+
/// the routed roam leg measured the engine's own first-valid-pair ->
|
|
1630
|
+
/// USE-CANDIDATE at 197 ms, one tick; the extra tick is headroom for the
|
|
1631
|
+
/// loopback's round trip and for a loaded machine. See the assertion in
|
|
1632
|
+
/// `a_restart_keeps_media_flowing_on_the_selected_pair_until_the_new_one_nominates`.
|
|
1633
|
+
const RECONNECT_BUDGET: Duration = Duration::from_millis(400);
|
|
1634
|
+
|
|
1635
|
+
/// A receiver peer that records when each inbound RTP packet arrives and
|
|
1636
|
+
/// reports its own ICE candidates, so two peers can really connect.
|
|
1637
|
+
struct MediaReceiver {
|
|
1638
|
+
candidates: tokio::sync::mpsc::UnboundedSender<RTCIceCandidateInit>,
|
|
1639
|
+
arrivals: Arc<Mutex<Vec<Instant>>>,
|
|
1640
|
+
connected: Arc<AtomicBool>,
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
#[async_trait::async_trait]
|
|
1644
|
+
impl PeerConnectionEventHandler for MediaReceiver {
|
|
1645
|
+
async fn on_ice_candidate(&self, event: RTCPeerConnectionIceEvent) {
|
|
1646
|
+
if let Ok(init) = event.candidate.to_json() {
|
|
1647
|
+
let _ = self.candidates.send(init);
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
async fn on_connection_state_change(&self, state: RTCPeerConnectionState) {
|
|
1652
|
+
self.connected.store(
|
|
1653
|
+
matches!(state, RTCPeerConnectionState::Connected),
|
|
1654
|
+
Ordering::SeqCst,
|
|
1655
|
+
);
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
async fn on_track(&self, track: Arc<dyn TrackRemote>) {
|
|
1659
|
+
let arrivals = Arc::clone(&self.arrivals);
|
|
1660
|
+
tokio::spawn(async move {
|
|
1661
|
+
while let Some(event) = track.poll().await {
|
|
1662
|
+
if matches!(event, TrackRemoteEvent::OnRtpPacket(_)) {
|
|
1663
|
+
arrivals.lock().unwrap().push(Instant::now());
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
});
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
async fn media_receiver(receiver: Arc<MediaReceiver>) -> Arc<dyn PeerConnection> {
|
|
1671
|
+
let mut media_engine = MediaEngine::default();
|
|
1672
|
+
media_engine
|
|
1673
|
+
.register_codec(vp9_codec(), RtpCodecKind::Video)
|
|
1674
|
+
.expect("the codec registers");
|
|
1675
|
+
let registry = register_default_interceptors(Registry::new(), &mut media_engine)
|
|
1676
|
+
.expect("interceptors register");
|
|
1677
|
+
let peer = PeerConnectionBuilder::<std::net::SocketAddr>::new()
|
|
1678
|
+
.with_media_engine(media_engine)
|
|
1679
|
+
.with_interceptor_registry(registry)
|
|
1680
|
+
.with_runtime(default_runtime().expect("a runtime"))
|
|
1681
|
+
.with_handler(receiver)
|
|
1682
|
+
.with_udp_addrs(vec![std::net::SocketAddr::from(([0, 0, 0, 0], 0))])
|
|
1683
|
+
.build()
|
|
1684
|
+
.await
|
|
1685
|
+
.expect("a receiving peer");
|
|
1686
|
+
Arc::new(peer)
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
/// A restart offer/answer round must not interrupt media already flowing on
|
|
1690
|
+
/// the selected pair: the picture keeps advancing (no gap over 200 ms) while
|
|
1691
|
+
/// the old pair stays reachable, and the ICE generation still turns over.
|
|
1692
|
+
#[tokio::test]
|
|
1693
|
+
async fn a_restart_keeps_media_flowing_on_the_selected_pair_until_the_new_one_nominates() {
|
|
1694
|
+
let (events, mut events_rx) = tokio::sync::mpsc::unbounded_channel();
|
|
1695
|
+
let (engine, offer) = VideoPeer::offer(transport(), events, VideoCodec::Vp9)
|
|
1696
|
+
.await
|
|
1697
|
+
.expect("an offering peer");
|
|
1698
|
+
let engine = Arc::new(engine);
|
|
1699
|
+
|
|
1700
|
+
let arrivals: Arc<Mutex<Vec<Instant>>> = Arc::new(Mutex::new(Vec::new()));
|
|
1701
|
+
let connected = Arc::new(AtomicBool::new(false));
|
|
1702
|
+
let (client_candidates_tx, mut client_candidates_rx) =
|
|
1703
|
+
tokio::sync::mpsc::unbounded_channel();
|
|
1704
|
+
let receiver = Arc::new(MediaReceiver {
|
|
1705
|
+
candidates: client_candidates_tx,
|
|
1706
|
+
arrivals: Arc::clone(&arrivals),
|
|
1707
|
+
connected: Arc::clone(&connected),
|
|
1708
|
+
});
|
|
1709
|
+
let client = media_receiver(Arc::clone(&receiver)).await;
|
|
1710
|
+
|
|
1711
|
+
client
|
|
1712
|
+
.set_remote_description(RTCSessionDescription::offer(offer.clone()).expect("an offer"))
|
|
1713
|
+
.await
|
|
1714
|
+
.expect("the offer applies");
|
|
1715
|
+
let answer = client.create_answer(None).await.expect("an answer");
|
|
1716
|
+
client
|
|
1717
|
+
.set_local_description(answer.clone())
|
|
1718
|
+
.await
|
|
1719
|
+
.expect("a local answer");
|
|
1720
|
+
engine
|
|
1721
|
+
.accept_answer(answer.sdp.clone())
|
|
1722
|
+
.await
|
|
1723
|
+
.expect("the answer is accepted");
|
|
1724
|
+
|
|
1725
|
+
// The engine's candidates reach the client.
|
|
1726
|
+
let client_for_engine_candidates = Arc::clone(&client);
|
|
1727
|
+
tokio::spawn(async move {
|
|
1728
|
+
while let Some(event) = events_rx.recv().await {
|
|
1729
|
+
if let PeerEvent::Candidate {
|
|
1730
|
+
candidate,
|
|
1731
|
+
sdp_mid,
|
|
1732
|
+
sdp_m_line_index,
|
|
1733
|
+
} = event
|
|
1734
|
+
{
|
|
1735
|
+
let _ = client_for_engine_candidates
|
|
1736
|
+
.add_ice_candidate(RTCIceCandidateInit {
|
|
1737
|
+
candidate,
|
|
1738
|
+
sdp_mid,
|
|
1739
|
+
sdp_mline_index: sdp_m_line_index,
|
|
1740
|
+
username_fragment: None,
|
|
1741
|
+
url: None,
|
|
1742
|
+
})
|
|
1743
|
+
.await;
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
});
|
|
1747
|
+
// The client's candidates reach the engine.
|
|
1748
|
+
let engine_for_client_candidates = Arc::clone(&engine);
|
|
1749
|
+
tokio::spawn(async move {
|
|
1750
|
+
while let Some(init) = client_candidates_rx.recv().await {
|
|
1751
|
+
let _ = engine_for_client_candidates
|
|
1752
|
+
.add_candidate(init.candidate, init.sdp_mid, init.sdp_mline_index)
|
|
1753
|
+
.await;
|
|
1754
|
+
}
|
|
1755
|
+
});
|
|
1756
|
+
|
|
1757
|
+
// Wait for the ICE path to come up on both ends.
|
|
1758
|
+
let deadline = Instant::now() + Duration::from_secs(5);
|
|
1759
|
+
while !(engine.is_connected() && connected.load(Ordering::SeqCst)) {
|
|
1760
|
+
assert!(Instant::now() < deadline, "the peers never connected");
|
|
1761
|
+
tokio::time::sleep(Duration::from_millis(10)).await;
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
// Any bytes the VP9 packetizer turns into one access unit.
|
|
1765
|
+
let frame: Vec<u8> = (0..200u32).map(|i| (i % 251) as u8).collect();
|
|
1766
|
+
let sending = Arc::new(AtomicBool::new(true));
|
|
1767
|
+
let engine_for_sending = Arc::clone(&engine);
|
|
1768
|
+
let sending_for_task = Arc::clone(&sending);
|
|
1769
|
+
let frame_for_task = frame.clone();
|
|
1770
|
+
tokio::spawn(async move {
|
|
1771
|
+
while sending_for_task.load(Ordering::SeqCst) {
|
|
1772
|
+
let _ = engine_for_sending
|
|
1773
|
+
.send_frame(&frame_for_task, true, Instant::now())
|
|
1774
|
+
.await;
|
|
1775
|
+
tokio::time::sleep(Duration::from_millis(5)).await;
|
|
1776
|
+
}
|
|
1777
|
+
});
|
|
1778
|
+
|
|
1779
|
+
// Let the picture flow, then take a clean baseline.
|
|
1780
|
+
tokio::time::sleep(Duration::from_millis(300)).await;
|
|
1781
|
+
arrivals.lock().unwrap().clear();
|
|
1782
|
+
tokio::time::sleep(Duration::from_millis(200)).await;
|
|
1783
|
+
let flowing = arrivals.lock().unwrap().len();
|
|
1784
|
+
assert!(
|
|
1785
|
+
flowing >= 5,
|
|
1786
|
+
"media was not flowing before the restart: {flowing} packets"
|
|
1787
|
+
);
|
|
1788
|
+
|
|
1789
|
+
// The restart a client drives when its carrier comes back on a roam.
|
|
1790
|
+
let restart = engine.restart_ice().await.expect("a restart offer");
|
|
1791
|
+
let ufrag = |sdp: &str| {
|
|
1792
|
+
sdp.lines()
|
|
1793
|
+
.find_map(|line| line.strip_prefix("a=ice-ufrag:"))
|
|
1794
|
+
.expect("ICE credentials")
|
|
1795
|
+
.to_owned()
|
|
1796
|
+
};
|
|
1797
|
+
assert_ne!(
|
|
1798
|
+
ufrag(&restart),
|
|
1799
|
+
ufrag(&offer),
|
|
1800
|
+
"the restart must turn the ICE generation over"
|
|
1801
|
+
);
|
|
1802
|
+
|
|
1803
|
+
let restart_at = Instant::now();
|
|
1804
|
+
arrivals.lock().unwrap().clear();
|
|
1805
|
+
|
|
1806
|
+
// The restart offer crosses the consumer's signaling channel before the
|
|
1807
|
+
// client answers, and the engine applies the restart at
|
|
1808
|
+
// `set_local_description` — inside `restart_ice` above, before the offer
|
|
1809
|
+
// has even left. It must keep feeding media on the old pair for that
|
|
1810
|
+
// whole round trip, not only until its own teardown. A real channel
|
|
1811
|
+
// costs more than a loopback hop, so hold this window for the bar's
|
|
1812
|
+
// length: a teardown shows up here, a preserved pair does not.
|
|
1813
|
+
tokio::time::sleep(Duration::from_millis(300)).await;
|
|
1814
|
+
|
|
1815
|
+
client
|
|
1816
|
+
.set_remote_description(RTCSessionDescription::offer(restart).expect("a restart offer"))
|
|
1817
|
+
.await
|
|
1818
|
+
.expect("the restart offer applies");
|
|
1819
|
+
let reanswer = client.create_answer(None).await.expect("an answer");
|
|
1820
|
+
client
|
|
1821
|
+
.set_local_description(reanswer.clone())
|
|
1822
|
+
.await
|
|
1823
|
+
.expect("a local answer");
|
|
1824
|
+
engine
|
|
1825
|
+
.accept_answer(reanswer.sdp)
|
|
1826
|
+
.await
|
|
1827
|
+
.expect("the restart answer is accepted");
|
|
1828
|
+
let answered_at = Instant::now();
|
|
1829
|
+
|
|
1830
|
+
// The restarted generation has to be nominated off its own check cycle:
|
|
1831
|
+
// nomination is decided in the agent's `contact_candidates` pass, which the
|
|
1832
|
+
// driver reaches on a `check_interval` tick (200 ms) or when an inbound
|
|
1833
|
+
// Binding request arrives while a valid pair already exists. A pair that
|
|
1834
|
+
// becomes valid just after a tick would therefore wait a full tick for its
|
|
1835
|
+
// USE-CANDIDATE, and one tick is the whole engine-side cost measured on the
|
|
1836
|
+
// routed roam leg (first valid pair -> USE-CANDIDATE: 197 ms, i.e. one tick).
|
|
1837
|
+
//
|
|
1838
|
+
// What this asserts, and what it cannot: the engine's "first valid pair"
|
|
1839
|
+
// instant is not observable from here (the peer connection exposes no pair
|
|
1840
|
+
// state), so the bound is measured from the answer, the earliest instant the
|
|
1841
|
+
// engine could nominate, at two ticks. The loopback's client answers checks
|
|
1842
|
+
// and sends its own triggered checks in the same burst, so an inbound request
|
|
1843
|
+
// is nearly always available and the base engine meets this bound too; the
|
|
1844
|
+
// tick itself is graded by the routed roam leg, whose relay log carries both
|
|
1845
|
+
// instants (197 ms, one tick). This guards the property going forward: a
|
|
1846
|
+
// lost pass, a skipped tick or a stalled nomination blows the bound.
|
|
1847
|
+
let reconnected = async {
|
|
1848
|
+
while !engine.is_connected() {
|
|
1849
|
+
tokio::time::sleep(Duration::from_millis(5)).await;
|
|
1850
|
+
}
|
|
1851
|
+
answered_at.elapsed()
|
|
1852
|
+
};
|
|
1853
|
+
let reconnect = tokio::time::timeout(RECONNECT_BUDGET, reconnected)
|
|
1854
|
+
.await
|
|
1855
|
+
.expect("the restarted generation was never nominated");
|
|
1856
|
+
eprintln!(
|
|
1857
|
+
"restart: transport reconnected {reconnect:?} after the answer (budget {RECONNECT_BUDGET:?})"
|
|
1858
|
+
);
|
|
1859
|
+
|
|
1860
|
+
// Keep the picture advancing across and after the restart.
|
|
1861
|
+
tokio::time::sleep(Duration::from_millis(800)).await;
|
|
1862
|
+
sending.store(false, Ordering::SeqCst);
|
|
1863
|
+
|
|
1864
|
+
let stamps = arrivals.lock().unwrap().clone();
|
|
1865
|
+
assert!(
|
|
1866
|
+
!stamps.is_empty(),
|
|
1867
|
+
"no media arrived at all after the restart"
|
|
1868
|
+
);
|
|
1869
|
+
let mut previous = restart_at;
|
|
1870
|
+
let mut worst = Duration::ZERO;
|
|
1871
|
+
for stamp in &stamps {
|
|
1872
|
+
worst = worst.max(stamp.duration_since(previous));
|
|
1873
|
+
previous = *stamp;
|
|
1874
|
+
}
|
|
1875
|
+
eprintln!(
|
|
1876
|
+
"restart: {} packets after the offer, worst media gap {worst:?}",
|
|
1877
|
+
stamps.len()
|
|
1878
|
+
);
|
|
1879
|
+
assert!(
|
|
1880
|
+
worst < Duration::from_millis(200),
|
|
1881
|
+
"media stalled for {worst:?} through the restart: {:?}",
|
|
1882
|
+
stamps
|
|
1883
|
+
);
|
|
1884
|
+
|
|
1885
|
+
engine.close().await;
|
|
1886
|
+
let _ = client.close().await;
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1592
1889
|
#[tokio::test]
|
|
1593
1890
|
async fn a_forwarded_client_is_offered_ice_over_tcp_on_the_loopback() {
|
|
1594
1891
|
let offered = |loopback_tcp: bool| async move {
|
package/engine/src/portal.rs
CHANGED
|
@@ -142,21 +142,3 @@ fn choose_cursor_mode(available: BitFlags<CursorMode>) -> Result<CursorMode> {
|
|
|
142
142
|
.find(|mode| available.contains(*mode))
|
|
143
143
|
.context("cursor-unavailable: portal cannot leave the cursor out of capture")
|
|
144
144
|
}
|
|
145
|
-
|
|
146
|
-
#[cfg(test)]
|
|
147
|
-
mod tests {
|
|
148
|
-
use super::*;
|
|
149
|
-
#[test]
|
|
150
|
-
fn hidden_takes_any_cursor_free_mode_and_refuses_embedded_only_portals() {
|
|
151
|
-
assert_eq!(
|
|
152
|
-
choose_cursor_mode(CursorMode::Metadata | CursorMode::Hidden).unwrap(),
|
|
153
|
-
CursorMode::Hidden
|
|
154
|
-
);
|
|
155
|
-
assert_eq!(
|
|
156
|
-
choose_cursor_mode(CursorMode::Metadata.into()).unwrap(),
|
|
157
|
-
CursorMode::Metadata
|
|
158
|
-
);
|
|
159
|
-
let error = choose_cursor_mode(CursorMode::Embedded.into()).unwrap_err();
|
|
160
|
-
assert!(error.to_string().contains("cursor-unavailable:"));
|
|
161
|
-
}
|
|
162
|
-
}
|
package/engine/src/protocol.rs
CHANGED
|
@@ -144,6 +144,11 @@ pub struct OpenParams {
|
|
|
144
144
|
/// the engine then spends nothing per frame on either.
|
|
145
145
|
#[serde(default = "default_local_frames")]
|
|
146
146
|
pub local_frames: bool,
|
|
147
|
+
/// Capture the desktop's own system audio for `session.audio`. Absent means
|
|
148
|
+
/// video only; there is never a silent downgrade, so a caller that asks and
|
|
149
|
+
/// cannot be answered is refused.
|
|
150
|
+
#[serde(default)]
|
|
151
|
+
pub audio: bool,
|
|
147
152
|
}
|
|
148
153
|
|
|
149
154
|
fn default_local_frames() -> bool {
|
|
@@ -244,6 +249,23 @@ pub struct SessionRef {
|
|
|
244
249
|
pub generation: Option<u64>,
|
|
245
250
|
}
|
|
246
251
|
|
|
252
|
+
/// Read the desktop's own audio, the way `FrameParams` reads a frame: bytes go
|
|
253
|
+
/// to the caller's path and JSON carries only metadata.
|
|
254
|
+
#[derive(Debug, Deserialize)]
|
|
255
|
+
pub struct AudioParams {
|
|
256
|
+
pub session_id: String,
|
|
257
|
+
#[serde(default)]
|
|
258
|
+
pub path: String,
|
|
259
|
+
/// The most bytes to write. Bounded to whole sample frames, so a truncated
|
|
260
|
+
/// chunk never ends mid-sample.
|
|
261
|
+
#[serde(default = "default_max_audio_bytes")]
|
|
262
|
+
pub max_bytes: usize,
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
fn default_max_audio_bytes() -> usize {
|
|
266
|
+
1 << 20
|
|
267
|
+
}
|
|
268
|
+
|
|
247
269
|
#[derive(Debug, Deserialize)]
|
|
248
270
|
pub struct DescriptionParams {
|
|
249
271
|
pub session_id: String,
|
|
@@ -395,168 +417,6 @@ pub enum ControlReply<'a> {
|
|
|
395
417
|
},
|
|
396
418
|
}
|
|
397
419
|
|
|
398
|
-
#[cfg(test)]
|
|
399
|
-
mod tests {
|
|
400
|
-
use super::*;
|
|
401
|
-
|
|
402
|
-
#[test]
|
|
403
|
-
fn cursor_option_defaults_and_refuses_unknown_modes() {
|
|
404
|
-
let default: OpenParams = serde_json::from_str("{}").unwrap();
|
|
405
|
-
assert_eq!(default.cursor, CursorMode::Embedded);
|
|
406
|
-
let hidden: OpenParams = serde_json::from_str(r#"{"cursor":"hidden"}"#).unwrap();
|
|
407
|
-
assert_eq!(hidden.cursor, CursorMode::Hidden);
|
|
408
|
-
assert!(serde_json::from_str::<OpenParams>(r#"{"cursor":"metadata"}"#).is_err());
|
|
409
|
-
assert!(serde_json::from_str::<OpenParams>(r#"{"cursor":false}"#).is_err());
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
#[test]
|
|
413
|
-
fn a_pointer_message_round_trips_with_its_sequence() {
|
|
414
|
-
let parsed: ControlMessage =
|
|
415
|
-
serde_json::from_str(r#"{"kind":"pointer","phase":"down","x":10,"y":20,"seq":3}"#)
|
|
416
|
-
.unwrap();
|
|
417
|
-
match parsed {
|
|
418
|
-
ControlMessage::Pointer {
|
|
419
|
-
phase,
|
|
420
|
-
x,
|
|
421
|
-
y,
|
|
422
|
-
seq,
|
|
423
|
-
button,
|
|
424
|
-
} => {
|
|
425
|
-
assert!(matches!(phase, PointerPhase::Down));
|
|
426
|
-
assert_eq!((x, y, seq, button), (10, 20, 3, 1));
|
|
427
|
-
}
|
|
428
|
-
other => panic!("wrong variant: {other:?}"),
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
#[test]
|
|
433
|
-
fn an_unknown_action_is_refused_rather_than_ignored() {
|
|
434
|
-
assert!(serde_json::from_str::<ControlMessage>(r#"{"kind":"teleport"}"#).is_err());
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
#[test]
|
|
438
|
-
fn display_source_round_trips_its_explicit_id() {
|
|
439
|
-
let request: SourceRequest = serde_json::from_value(serde_json::json!({
|
|
440
|
-
"kind": "display",
|
|
441
|
-
"display_id": 42
|
|
442
|
-
}))
|
|
443
|
-
.unwrap();
|
|
444
|
-
assert!(matches!(
|
|
445
|
-
request,
|
|
446
|
-
SourceRequest::Display {
|
|
447
|
-
display_id: Some(42)
|
|
448
|
-
}
|
|
449
|
-
));
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
#[test]
|
|
453
|
-
fn an_encoded_source_names_its_codec_and_its_own_size() {
|
|
454
|
-
let request: SourceRequest = serde_json::from_value(serde_json::json!({
|
|
455
|
-
"kind": "encoded",
|
|
456
|
-
"codec": "h264",
|
|
457
|
-
"width": 486,
|
|
458
|
-
"height": 1080
|
|
459
|
-
}))
|
|
460
|
-
.unwrap();
|
|
461
|
-
assert!(matches!(
|
|
462
|
-
request,
|
|
463
|
-
SourceRequest::Encoded {
|
|
464
|
-
codec: EncodedCodec::H264,
|
|
465
|
-
width: 486,
|
|
466
|
-
height: 1080
|
|
467
|
-
}
|
|
468
|
-
));
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
#[test]
|
|
472
|
-
fn an_unknown_encoded_codec_is_refused_rather_than_guessed() {
|
|
473
|
-
assert!(serde_json::from_value::<SourceRequest>(serde_json::json!({
|
|
474
|
-
"kind": "encoded",
|
|
475
|
-
"codec": "av1",
|
|
476
|
-
"width": 486,
|
|
477
|
-
"height": 1080
|
|
478
|
-
}))
|
|
479
|
-
.is_err());
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
#[test]
|
|
483
|
-
fn a_feed_request_is_one_access_unit_with_its_own_keyframe_flag() {
|
|
484
|
-
let params: FeedParams =
|
|
485
|
-
serde_json::from_str(r#"{"session_id":"s","keyframe":true,"data_b64":"AAAA"}"#)
|
|
486
|
-
.unwrap();
|
|
487
|
-
assert_eq!(params.session_id, "s");
|
|
488
|
-
assert!(params.keyframe, "a feed says whether its unit is an IDR");
|
|
489
|
-
assert_eq!(params.data_b64, "AAAA");
|
|
490
|
-
assert_eq!(params.access_unit().unwrap(), vec![0x00, 0x00, 0x00]);
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
#[test]
|
|
494
|
-
fn a_feed_request_that_is_not_base64_is_refused() {
|
|
495
|
-
let params = FeedParams {
|
|
496
|
-
session_id: String::from("s"),
|
|
497
|
-
keyframe: false,
|
|
498
|
-
data_b64: String::from("not base64!!"),
|
|
499
|
-
};
|
|
500
|
-
assert!(params.access_unit().is_err());
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
/// The reason this protocol carries bytes in base64 rather than adding a
|
|
504
|
-
/// second inherited fd: at 2 Mbit/s it costs a fraction of a per cent of a
|
|
505
|
-
/// core, against a whole second channel to keep ordered and bounded. The
|
|
506
|
-
/// bound below is deliberately loose — it is a sanity check on the order of
|
|
507
|
-
/// magnitude, not a benchmark, so it does not flake on a loaded machine.
|
|
508
|
-
#[test]
|
|
509
|
-
fn decoding_a_two_megabit_stream_is_nowhere_near_a_core() {
|
|
510
|
-
// One second of a 2 Mbit/s stream, in access units.
|
|
511
|
-
let unit: Vec<u8> = (0..8_000u32).map(|index| index as u8).collect();
|
|
512
|
-
let encoded = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &unit);
|
|
513
|
-
let params = FeedParams {
|
|
514
|
-
session_id: String::from("s"),
|
|
515
|
-
keyframe: false,
|
|
516
|
-
data_b64: encoded,
|
|
517
|
-
};
|
|
518
|
-
let started = std::time::Instant::now();
|
|
519
|
-
for _ in 0..(250_000 / unit.len()) {
|
|
520
|
-
assert_eq!(params.access_unit().unwrap().len(), unit.len());
|
|
521
|
-
}
|
|
522
|
-
let elapsed = started.elapsed();
|
|
523
|
-
assert!(
|
|
524
|
-
elapsed < std::time::Duration::from_millis(500),
|
|
525
|
-
"a second of 2 Mbit/s base64 took {elapsed:?}, which is far past 5 % of a core",
|
|
526
|
-
);
|
|
527
|
-
println!("a second of 2 Mbit/s stream decoded in {elapsed:?}");
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
#[test]
|
|
531
|
-
fn open_defaults_keep_the_desktops_own_pixels_up_to_4k() {
|
|
532
|
-
for payload in [
|
|
533
|
-
r#"{}"#,
|
|
534
|
-
r#"{"permissions":["view"]}"#,
|
|
535
|
-
r#"{"permissions":["view","unknown-legacy-scope"]}"#,
|
|
536
|
-
] {
|
|
537
|
-
let _: OpenParams = serde_json::from_str(payload).unwrap();
|
|
538
|
-
}
|
|
539
|
-
let params: OpenParams =
|
|
540
|
-
serde_json::from_str(r#"{"permissions":["view","control"]}"#).unwrap();
|
|
541
|
-
assert_eq!(params.max_width, 3840);
|
|
542
|
-
assert_eq!(params.max_height, 2160);
|
|
543
|
-
assert_eq!(
|
|
544
|
-
params.bitrate_kbps, 0,
|
|
545
|
-
"the engine sizes the rate to the surface"
|
|
546
|
-
);
|
|
547
|
-
assert_eq!(params.max_fps, 60);
|
|
548
|
-
assert!(params.local_frames, "session.frame works unless turned off");
|
|
549
|
-
assert!(
|
|
550
|
-
!params.agent_indicator,
|
|
551
|
-
"human control has no overlay by default"
|
|
552
|
-
);
|
|
553
|
-
let agent: OpenParams =
|
|
554
|
-
serde_json::from_str(r#"{"permissions":["view","control"],"agent_indicator":true}"#)
|
|
555
|
-
.unwrap();
|
|
556
|
-
assert!(agent.agent_indicator);
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
|
|
560
420
|
/// Monotonic engine-process clock shared by local frame and cursor notices.
|
|
561
421
|
pub fn timestamp_us(at: std::time::Instant) -> u64 {
|
|
562
422
|
static EPOCH: std::sync::OnceLock<std::time::Instant> = std::sync::OnceLock::new();
|