@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/mac.rs
CHANGED
|
@@ -39,6 +39,8 @@ unsafe extern "C" {
|
|
|
39
39
|
pub(crate) fn CGPreflightScreenCaptureAccess() -> bool;
|
|
40
40
|
pub(crate) fn CGPreflightPostEventAccess() -> bool;
|
|
41
41
|
pub(crate) fn CGRequestPostEventAccess() -> bool;
|
|
42
|
+
pub(crate) fn CGPreflightListenEventAccess() -> bool;
|
|
43
|
+
pub(crate) fn CGRequestListenEventAccess() -> bool;
|
|
42
44
|
fn CGGetActiveDisplayList(max: u32, displays: *mut DisplayId, count: *mut u32) -> i32;
|
|
43
45
|
fn CGMainDisplayID() -> DisplayId;
|
|
44
46
|
fn CGDisplayPixelsWide(display: DisplayId) -> usize;
|
|
@@ -713,6 +715,12 @@ pub fn run() -> i32 {
|
|
|
713
715
|
let _ = crate::input::request_access();
|
|
714
716
|
println!("If the Accessibility alert did not open, allow {} in System Settings › Privacy & Security › Accessibility.", tcc_responsible_app_name());
|
|
715
717
|
}
|
|
718
|
+
if crate::input::observe_probe().is_ok() {
|
|
719
|
+
println!("Input Monitoring permission is already granted to {}.", tcc_responsible_app_name());
|
|
720
|
+
} else {
|
|
721
|
+
let _ = crate::input::request_observe_access();
|
|
722
|
+
println!("If the Input Monitoring alert did not open, allow {} in System Settings › Privacy & Security › Input Monitoring.", tcc_responsible_app_name());
|
|
723
|
+
}
|
|
716
724
|
}
|
|
717
725
|
_ => println!("desklink-host {}\nUSAGE: desklink-host [serve|capabilities|capture-probe|encode-probe|setup-input|version]", env!("CARGO_PKG_VERSION")),
|
|
718
726
|
}
|
package/engine/src/main.rs
CHANGED
|
@@ -26,7 +26,7 @@ mod clipboard;
|
|
|
26
26
|
mod clipboard;
|
|
27
27
|
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
|
|
28
28
|
mod convert;
|
|
29
|
-
#[cfg(target_os = "linux")]
|
|
29
|
+
#[cfg(any(target_os = "linux", all(target_os = "macos", desklink_vpx)))]
|
|
30
30
|
mod cursor;
|
|
31
31
|
#[cfg(target_os = "linux")]
|
|
32
32
|
mod encoder;
|
|
@@ -72,6 +72,8 @@ mod keymap;
|
|
|
72
72
|
mod keymap;
|
|
73
73
|
#[cfg(target_os = "macos")]
|
|
74
74
|
mod mac;
|
|
75
|
+
#[cfg(any(target_os = "linux", all(any(target_os = "macos", target_os = "windows"), desklink_vpx)))]
|
|
76
|
+
mod observe;
|
|
75
77
|
#[cfg(any(
|
|
76
78
|
target_os = "linux",
|
|
77
79
|
all(any(target_os = "macos", target_os = "windows"), desklink_vpx)
|
|
@@ -136,7 +138,14 @@ use tokio::sync::mpsc as tokio_mpsc;
|
|
|
136
138
|
unsafe extern "C" {
|
|
137
139
|
fn desklink_point_overlay_main(display: *const std::ffi::c_char) -> i32;
|
|
138
140
|
fn desklink_agent_overlay_main(display: *const std::ffi::c_char) -> i32;
|
|
139
|
-
fn desklink_agent_overlay_wayland(
|
|
141
|
+
fn desklink_agent_overlay_wayland(
|
|
142
|
+
source_w: i32,
|
|
143
|
+
source_h: i32,
|
|
144
|
+
desktop: i32,
|
|
145
|
+
origin_known: i32,
|
|
146
|
+
origin_x: i32,
|
|
147
|
+
origin_y: i32,
|
|
148
|
+
) -> i32;
|
|
140
149
|
fn desklink_wayland_layer_shell_available() -> i32;
|
|
141
150
|
fn desklink_point_overlay_wayland(
|
|
142
151
|
w: i32,
|
|
@@ -212,7 +221,21 @@ fn main() {
|
|
|
212
221
|
2
|
|
213
222
|
} else {
|
|
214
223
|
let desktop = i32::from(args.get(3).map(String::as_str) == Some("desktop"));
|
|
215
|
-
|
|
224
|
+
// A desktop cue respawned after a press names the layout
|
|
225
|
+
// point that chose its output: `w h desktop ox oy`.
|
|
226
|
+
let (known, ox, oy) = match (args.get(4), args.get(5)) {
|
|
227
|
+
(Some(x), Some(y)) => match (x.parse(), y.parse()) {
|
|
228
|
+
(Ok(x), Ok(y)) => (1, x, y),
|
|
229
|
+
_ => (0, 0, 0),
|
|
230
|
+
},
|
|
231
|
+
_ => (0, 0, 0),
|
|
232
|
+
};
|
|
233
|
+
let (known, ox, oy) = if desktop == 1 {
|
|
234
|
+
(known, ox, oy)
|
|
235
|
+
} else {
|
|
236
|
+
(0, 0, 0)
|
|
237
|
+
};
|
|
238
|
+
unsafe { desklink_agent_overlay_wayland(width, height, desktop, known, ox, oy) }
|
|
216
239
|
}
|
|
217
240
|
}
|
|
218
241
|
"capabilities" => {
|
|
@@ -542,6 +565,47 @@ async fn serve() -> Result<()> {
|
|
|
542
565
|
Ok(())
|
|
543
566
|
}
|
|
544
567
|
|
|
568
|
+
/// Splits the requested ICE servers into the configuration the peer gets and
|
|
569
|
+
/// the URLs this engine leaves out. Every URL the WebRTC stack can dial goes
|
|
570
|
+
/// through with its entry's username and credential — all of an entry's URLs,
|
|
571
|
+
/// so a fallback behind an unreachable primary still gathers candidates. A
|
|
572
|
+
/// TURN URL whose transport is not UDP is one the stack cannot dial, so it is
|
|
573
|
+
/// skipped here and reported once by the caller, never dropped silently. A
|
|
574
|
+
/// URL the parser cannot read is forwarded unchanged: the peer connection
|
|
575
|
+
/// owns that verdict and refuses the open in its own words.
|
|
576
|
+
#[cfg(any(
|
|
577
|
+
target_os = "linux",
|
|
578
|
+
all(any(target_os = "macos", target_os = "windows"), desklink_vpx)
|
|
579
|
+
))]
|
|
580
|
+
fn usable_ice_servers(
|
|
581
|
+
servers: Vec<protocol::IceServerParam>,
|
|
582
|
+
) -> (
|
|
583
|
+
Vec<(Vec<String>, Option<String>, Option<String>)>,
|
|
584
|
+
Vec<String>,
|
|
585
|
+
) {
|
|
586
|
+
use rtc_ice::url::{ProtoType, SchemeType, Url};
|
|
587
|
+
let mut usable = Vec::new();
|
|
588
|
+
let mut skipped = Vec::new();
|
|
589
|
+
for server in servers {
|
|
590
|
+
let mut urls = Vec::with_capacity(server.urls.len());
|
|
591
|
+
for url in server.urls {
|
|
592
|
+
let undialable = Url::parse_url(&url).is_ok_and(|parsed| {
|
|
593
|
+
matches!(parsed.scheme, SchemeType::Turn | SchemeType::Turns)
|
|
594
|
+
&& parsed.proto != ProtoType::Udp
|
|
595
|
+
});
|
|
596
|
+
if undialable {
|
|
597
|
+
skipped.push(url);
|
|
598
|
+
} else {
|
|
599
|
+
urls.push(url);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
if !urls.is_empty() {
|
|
603
|
+
usable.push((urls, server.username, server.credential));
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
(usable, skipped)
|
|
607
|
+
}
|
|
608
|
+
|
|
545
609
|
#[cfg(any(
|
|
546
610
|
target_os = "linux",
|
|
547
611
|
all(any(target_os = "macos", target_os = "windows"), desklink_vpx)
|
|
@@ -604,6 +668,13 @@ fn render_event(notice: session::Notice) -> Option<String> {
|
|
|
604
668
|
"session.input",
|
|
605
669
|
serde_json::json!({ "sessionId": session_id, "input": input }),
|
|
606
670
|
),
|
|
671
|
+
session::SessionEvent::Observed {
|
|
672
|
+
timestamp_us,
|
|
673
|
+
input,
|
|
674
|
+
} => (
|
|
675
|
+
"session.input.observed",
|
|
676
|
+
serde_json::json!({ "sessionId": session_id, "timestamp_us": timestamp_us, "input": input }),
|
|
677
|
+
),
|
|
607
678
|
session::SessionEvent::KeyframeRequest { generation } => (
|
|
608
679
|
"session.keyframeRequest",
|
|
609
680
|
serde_json::json!({ "sessionId": session_id, "generation": generation }),
|
|
@@ -616,6 +687,10 @@ fn render_event(notice: session::Notice) -> Option<String> {
|
|
|
616
687
|
"session.capture.stopped",
|
|
617
688
|
serde_json::json!({ "sessionId": session_id, "reason": reason }),
|
|
618
689
|
),
|
|
690
|
+
session::SessionEvent::IceServerSkipped { url } => (
|
|
691
|
+
"session.iceServerSkipped",
|
|
692
|
+
serde_json::json!({ "sessionId": session_id, "url": url }),
|
|
693
|
+
),
|
|
619
694
|
session::SessionEvent::Revoked { reason, code } => (
|
|
620
695
|
"session.revoked",
|
|
621
696
|
serde_json::json!({ "sessionId": session_id, "reason": reason, "code": code }),
|
|
@@ -672,6 +747,7 @@ async fn dispatch(
|
|
|
672
747
|
}
|
|
673
748
|
let params: protocol::OpenParams = serde_json::from_value(request.params.clone())
|
|
674
749
|
.map_err(|error| ErrorBody::new("malformed", error.to_string()))?;
|
|
750
|
+
let usable = usable_ice_servers(params.ice_servers);
|
|
675
751
|
let open = session::OpenRequest {
|
|
676
752
|
source: params.source,
|
|
677
753
|
cursor: params.cursor,
|
|
@@ -685,16 +761,8 @@ async fn dispatch(
|
|
|
685
761
|
agent_indicator: params.agent_indicator,
|
|
686
762
|
local_frames: params.local_frames,
|
|
687
763
|
audio: params.audio,
|
|
688
|
-
|
|
689
|
-
|
|
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(),
|
|
764
|
+
observe_input: params.observe_input,
|
|
765
|
+
ice_servers: usable.0,
|
|
698
766
|
};
|
|
699
767
|
let session = session::Session::open(open, events.clone())
|
|
700
768
|
.await
|
|
@@ -712,7 +780,17 @@ async fn dispatch(
|
|
|
712
780
|
"cursor": session.cursor_info(),
|
|
713
781
|
"agentIndicator": session.agent_indicator(),
|
|
714
782
|
"audio": session.audio_info(),
|
|
783
|
+
"inputObservation": session.observation_info(),
|
|
715
784
|
});
|
|
785
|
+
// URLs the WebRTC stack cannot dial were left out of the peer's
|
|
786
|
+
// configuration; each skipped one is named once instead of
|
|
787
|
+
// vanishing from a multi-URL entry without a trace.
|
|
788
|
+
for url in usable.1 {
|
|
789
|
+
let _ = events.send(session::Notice {
|
|
790
|
+
session_id: session.id().to_owned(),
|
|
791
|
+
event: session::SessionEvent::IceServerSkipped { url },
|
|
792
|
+
});
|
|
793
|
+
}
|
|
716
794
|
*current = Some(session);
|
|
717
795
|
Ok(result)
|
|
718
796
|
}
|
|
@@ -947,6 +1025,26 @@ mod tests {
|
|
|
947
1025
|
);
|
|
948
1026
|
}
|
|
949
1027
|
|
|
1028
|
+
#[test]
|
|
1029
|
+
fn the_skipped_ice_server_warning_names_the_url_once() {
|
|
1030
|
+
let line = render_event(session::Notice {
|
|
1031
|
+
session_id: String::from("session-1"),
|
|
1032
|
+
event: session::SessionEvent::IceServerSkipped {
|
|
1033
|
+
url: String::from("turn:turn.example.com:3478?transport=tcp"),
|
|
1034
|
+
},
|
|
1035
|
+
})
|
|
1036
|
+
.expect("a skipped-URL warning is renderable");
|
|
1037
|
+
let parsed: serde_json::Value = serde_json::from_str(&line).unwrap();
|
|
1038
|
+
assert_eq!(parsed["event"], "session.iceServerSkipped");
|
|
1039
|
+
assert_eq!(
|
|
1040
|
+
parsed["params"],
|
|
1041
|
+
serde_json::json!({
|
|
1042
|
+
"sessionId": "session-1",
|
|
1043
|
+
"url": "turn:turn.example.com:3478?transport=tcp"
|
|
1044
|
+
})
|
|
1045
|
+
);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
950
1048
|
#[tokio::test]
|
|
951
1049
|
async fn the_state_notification_carries_exactly_what_the_document_promises() {
|
|
952
1050
|
let line = render_event(session::Notice {
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
//! Passive observation of the desktop's own pointer buttons, cursor and keys.
|
|
2
|
+
//!
|
|
3
|
+
//! The engine only listens; events are the ones `docs/PROTOCOL.md` describes. A
|
|
4
|
+
//! platform that cannot observe says why with a typed reason and delivers nothing.
|
|
5
|
+
|
|
6
|
+
use crate::protocol::{CursorPosition, ObservedInput};
|
|
7
|
+
use std::sync::Arc;
|
|
8
|
+
|
|
9
|
+
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
|
|
10
|
+
pub enum Observation {
|
|
11
|
+
Input {
|
|
12
|
+
timestamp_us: u64,
|
|
13
|
+
input: ObservedInput,
|
|
14
|
+
},
|
|
15
|
+
Cursor(CursorPosition),
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
pub type Sink = Arc<dyn Fn(Observation) + Send + Sync + 'static>;
|
|
19
|
+
|
|
20
|
+
/// Keeps an observer running; dropping it stops it.
|
|
21
|
+
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
|
|
22
|
+
pub struct Tap {
|
|
23
|
+
pub mechanism: &'static str,
|
|
24
|
+
pub guard: Box<dyn Send>,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// Why the desktop's input cannot be observed, and what the owner can do.
|
|
28
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
29
|
+
pub struct Unavailable {
|
|
30
|
+
pub code: &'static str,
|
|
31
|
+
pub reason: String,
|
|
32
|
+
pub remedy: String,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
#[cfg(target_os = "macos")]
|
|
36
|
+
pub fn start(display_id: u32, pixels: (usize, usize), sink: Sink) -> Result<Tap, Unavailable> {
|
|
37
|
+
crate::input::observe(display_id, pixels, sink)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#[cfg(not(target_os = "macos"))]
|
|
41
|
+
pub fn start(_: u32, _: (usize, usize), _: Sink) -> Result<Tap, Unavailable> {
|
|
42
|
+
Err(Unavailable {
|
|
43
|
+
code: "unsupported-platform",
|
|
44
|
+
reason: String::from("observing the desktop's own input is only implemented on macOS"),
|
|
45
|
+
remedy: String::from("none: this platform delivers no input events"),
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// The `inputObservation` member of the `session.open` reply.
|
|
50
|
+
pub fn status(result: Option<&Result<Tap, Unavailable>>) -> serde_json::Value {
|
|
51
|
+
match result {
|
|
52
|
+
None => serde_json::json!({"observing": false}),
|
|
53
|
+
Some(Ok(tap)) => serde_json::json!({"observing": true, "mechanism": tap.mechanism}),
|
|
54
|
+
Some(Err(error)) => serde_json::json!({
|
|
55
|
+
"observing": false,
|
|
56
|
+
"code": error.code,
|
|
57
|
+
"reason": error.reason,
|
|
58
|
+
"remedy": error.remedy,
|
|
59
|
+
}),
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#[cfg(test)]
|
|
64
|
+
mod tests {
|
|
65
|
+
use super::*;
|
|
66
|
+
|
|
67
|
+
#[test]
|
|
68
|
+
#[cfg(not(target_os = "macos"))]
|
|
69
|
+
fn a_platform_that_cannot_observe_says_so_and_delivers_nothing() {
|
|
70
|
+
let delivered = Arc::new(std::sync::atomic::AtomicUsize::new(0));
|
|
71
|
+
let counted = delivered.clone();
|
|
72
|
+
let sink: Sink = Arc::new(move |_| {
|
|
73
|
+
counted.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
|
74
|
+
});
|
|
75
|
+
let refused = start(0, (1920, 1080), sink);
|
|
76
|
+
let status = status(Some(&refused));
|
|
77
|
+
assert_eq!(status["observing"], false);
|
|
78
|
+
assert_eq!(status["code"], "unsupported-platform");
|
|
79
|
+
assert_eq!(delivered.load(std::sync::atomic::Ordering::SeqCst), 0);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
#[test]
|
|
83
|
+
fn a_refusal_carries_its_code_reason_and_remedy() {
|
|
84
|
+
let refused: Result<Tap, Unavailable> = Err(Unavailable {
|
|
85
|
+
code: "missing-input-monitoring",
|
|
86
|
+
reason: String::from("reason"),
|
|
87
|
+
remedy: String::from("remedy"),
|
|
88
|
+
});
|
|
89
|
+
assert_eq!(
|
|
90
|
+
status(Some(&refused)),
|
|
91
|
+
serde_json::json!({"observing": false, "code": "missing-input-monitoring", "reason": "reason", "remedy": "remedy"})
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
package/engine/src/peer.rs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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(|(
|
|
902
|
-
urls:
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
1972
|
-
std::net::
|
|
1973
|
-
|
|
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
|
}
|