@desklink/host 0.5.3 → 0.5.4
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 +25 -17
- package/dist/engineProcess.d.ts +5 -0
- package/dist/engineProcess.d.ts.map +1 -1
- package/dist/engineProcess.js +5 -3
- package/dist/engineProcess.js.map +1 -1
- package/engine/Cargo.lock +6 -8
- package/engine/Cargo.toml +13 -1
- 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 +0 -28
- package/engine/src/peer.rs +224 -0
- package/engine/src/portal.rs +0 -18
- package/engine/src/protocol.rs +0 -162
- package/engine/src/x11.rs +0 -13
- package/engine/vendor/rtc-ice/Cargo.toml +137 -0
- package/engine/vendor/rtc-ice/DESKLINK-PATCH.md +59 -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 +566 -0
- package/engine/vendor/rtc-ice/src/agent/agent_stats.rs +292 -0
- package/engine/vendor/rtc-ice/src/agent/agent_test.rs +3388 -0
- package/engine/vendor/rtc-ice/src/agent/mod.rs +1662 -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/lib.rs +85 -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/keeper.rs
CHANGED
|
@@ -483,140 +483,3 @@ fn fatal(out: &mut impl Write, error: &str) -> Result<()> {
|
|
|
483
483
|
write_line(out, &serde_json::json!({ "error": error }).to_string())?;
|
|
484
484
|
Err(anyhow!("{error}"))
|
|
485
485
|
}
|
|
486
|
-
|
|
487
|
-
#[cfg(test)]
|
|
488
|
-
mod tests {
|
|
489
|
-
use super::*;
|
|
490
|
-
|
|
491
|
-
#[test]
|
|
492
|
-
fn content_fills_the_screen_and_helpers_are_parked() {
|
|
493
|
-
assert_eq!(kiosk_geometry(1240, 760, 1280, 800), (0, 0, 1280, 800));
|
|
494
|
-
// Exactly at the limit is content; one pixel under is not.
|
|
495
|
-
assert_eq!(kiosk_geometry(120, 120, 1280, 800), (0, 0, 1280, 800));
|
|
496
|
-
assert_eq!(
|
|
497
|
-
kiosk_geometry(119, 800, 1280, 800),
|
|
498
|
-
(i32::from(PARKED_XY), i32::from(PARKED_XY), 119, 800)
|
|
499
|
-
);
|
|
500
|
-
assert_eq!(
|
|
501
|
-
kiosk_geometry(1000, 24, 1280, 800),
|
|
502
|
-
(i32::from(PARKED_XY), i32::from(PARKED_XY), 1000, 24)
|
|
503
|
-
);
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
#[test]
|
|
507
|
-
fn wm_class_reads_the_class_not_the_instance() {
|
|
508
|
-
assert_eq!(
|
|
509
|
-
decode_wm_class(b"google-chrome\0Google-chrome\0"),
|
|
510
|
-
Some(String::from("Google-chrome"))
|
|
511
|
-
);
|
|
512
|
-
assert_eq!(decode_wm_class(b"instance-only\0"), None);
|
|
513
|
-
assert_eq!(decode_wm_class(b""), None);
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
#[test]
|
|
517
|
-
fn titles_come_from_either_name_property_and_blank_is_none() {
|
|
518
|
-
assert_eq!(
|
|
519
|
-
decode_title("Pricing — Acme Store"),
|
|
520
|
-
Some(String::from("Pricing — Acme Store"))
|
|
521
|
-
);
|
|
522
|
-
assert_eq!(
|
|
523
|
-
decode_title("PPF probe - Google Chrome"),
|
|
524
|
-
Some(String::from("PPF probe - Google Chrome"))
|
|
525
|
-
);
|
|
526
|
-
assert_eq!(decode_title(" "), None);
|
|
527
|
-
assert_eq!(decode_title(""), None);
|
|
528
|
-
assert_eq!(
|
|
529
|
-
decode_title(&latin1(b"Gr\xf6\xdfe")),
|
|
530
|
-
Some(String::from("Gr\u{00f6}\u{00df}e"))
|
|
531
|
-
);
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
#[test]
|
|
535
|
-
fn the_report_line_is_exactly_the_promised_shape() {
|
|
536
|
-
let line = window_line(&[KeeperWindow {
|
|
537
|
-
id: 0x0040_0001,
|
|
538
|
-
title: Some(String::from("PPF probe - Google Chrome")),
|
|
539
|
-
class: Some(String::from("Google-chrome")),
|
|
540
|
-
pid: Some(1003571),
|
|
541
|
-
width: 1280,
|
|
542
|
-
height: 800,
|
|
543
|
-
}]);
|
|
544
|
-
let parsed: serde_json::Value = serde_json::from_str(&line).unwrap();
|
|
545
|
-
assert_eq!(
|
|
546
|
-
parsed,
|
|
547
|
-
serde_json::json!({
|
|
548
|
-
"windows": [{
|
|
549
|
-
"id": 0x40_0001_u64,
|
|
550
|
-
"title": "PPF probe - Google Chrome",
|
|
551
|
-
"class": "Google-chrome",
|
|
552
|
-
"pid": 1003571,
|
|
553
|
-
"width": 1280,
|
|
554
|
-
"height": 800,
|
|
555
|
-
}]
|
|
556
|
-
})
|
|
557
|
-
);
|
|
558
|
-
assert_eq!(window_line(&[]), r#"{"windows":[]}"#);
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
#[test]
|
|
562
|
-
fn missing_fields_are_null_not_absent() {
|
|
563
|
-
let line = window_line(&[KeeperWindow {
|
|
564
|
-
id: 7,
|
|
565
|
-
title: None,
|
|
566
|
-
class: None,
|
|
567
|
-
pid: None,
|
|
568
|
-
width: 100,
|
|
569
|
-
height: 100,
|
|
570
|
-
}]);
|
|
571
|
-
let parsed: serde_json::Value = serde_json::from_str(&line).unwrap();
|
|
572
|
-
let window = &parsed["windows"][0];
|
|
573
|
-
assert_eq!(window["title"], serde_json::Value::Null);
|
|
574
|
-
assert_eq!(window["class"], serde_json::Value::Null);
|
|
575
|
-
assert_eq!(window["pid"], serde_json::Value::Null);
|
|
576
|
-
assert!(window.get("width").is_some());
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
#[test]
|
|
580
|
-
fn an_unopenable_display_is_refused_in_one_error_line() {
|
|
581
|
-
let mut out = Vec::new();
|
|
582
|
-
assert!(keep(Some("not a display"), &mut out).is_err());
|
|
583
|
-
let line: serde_json::Value =
|
|
584
|
-
serde_json::from_str(&String::from_utf8(out).unwrap()).unwrap();
|
|
585
|
-
assert!(line["error"]
|
|
586
|
-
.as_str()
|
|
587
|
-
.is_some_and(|error| !error.is_empty()));
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
#[test]
|
|
591
|
-
fn a_refused_keeper_says_why_in_its_error_line() {
|
|
592
|
-
let mut out = Vec::new();
|
|
593
|
-
let error = fatal(&mut out, "another window manager").unwrap_err();
|
|
594
|
-
assert_eq!(error.to_string(), "another window manager");
|
|
595
|
-
assert_eq!(
|
|
596
|
-
String::from_utf8(out).unwrap(),
|
|
597
|
-
"{\"error\":\"another window manager\"}\n"
|
|
598
|
-
);
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
#[test]
|
|
602
|
-
fn the_display_flag_has_one_spelling() {
|
|
603
|
-
let flag = |args: &[&str]| {
|
|
604
|
-
display_argument(
|
|
605
|
-
&args
|
|
606
|
-
.iter()
|
|
607
|
-
.map(|arg| String::from(*arg))
|
|
608
|
-
.collect::<Vec<_>>(),
|
|
609
|
-
)
|
|
610
|
-
};
|
|
611
|
-
assert_eq!(flag(&["--display", ":42"]), Some(String::from(":42")));
|
|
612
|
-
assert_eq!(
|
|
613
|
-
flag(&["--display", ":42", "--x"]),
|
|
614
|
-
Some(String::from(":42"))
|
|
615
|
-
);
|
|
616
|
-
assert_eq!(flag(&[]), None);
|
|
617
|
-
assert_eq!(flag(&["--display"]), None);
|
|
618
|
-
// The undocumented `--display=:42` spelling is not a display: `keep`
|
|
619
|
-
// refuses it rather than guessing what was meant.
|
|
620
|
-
assert_eq!(flag(&["--display=:42"]), None);
|
|
621
|
-
}
|
|
622
|
-
}
|
package/engine/src/keymap.rs
CHANGED
|
@@ -254,110 +254,3 @@ impl Layout {
|
|
|
254
254
|
(plan, unreachable)
|
|
255
255
|
}
|
|
256
256
|
}
|
|
257
|
-
|
|
258
|
-
#[cfg(test)]
|
|
259
|
-
mod tests {
|
|
260
|
-
use super::*;
|
|
261
|
-
|
|
262
|
-
#[test]
|
|
263
|
-
fn a_layout_that_comes_from_the_environment_is_the_one_compiled() {
|
|
264
|
-
let names = LayoutNames::from_lookup(|key| match key {
|
|
265
|
-
"XKB_DEFAULT_LAYOUT" => Some(String::from("de")),
|
|
266
|
-
_ => None,
|
|
267
|
-
});
|
|
268
|
-
assert_eq!(names.identity(), "de");
|
|
269
|
-
|
|
270
|
-
let layout = Layout::from_names(&names).expect("the German keymap should compile");
|
|
271
|
-
let (plan, unreachable) = layout.plan_text("z");
|
|
272
|
-
assert!(
|
|
273
|
-
unreachable.is_empty(),
|
|
274
|
-
"z should be reachable: {unreachable:?}"
|
|
275
|
-
);
|
|
276
|
-
// QWERTZ puts z where a US layout puts y, so the compiled keymap is the
|
|
277
|
-
// environment's, not the library default.
|
|
278
|
-
assert_eq!(plan[0][0].code, 21, "z must sit on the y key position");
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
#[test]
|
|
282
|
-
fn a_session_with_no_layout_variable_reports_the_library_default() {
|
|
283
|
-
assert_eq!(LayoutNames::from_lookup(|_| None).identity(), "us");
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
#[test]
|
|
287
|
-
fn a_character_is_planned_as_a_real_key_event_on_the_compiled_layout() {
|
|
288
|
-
let layout = Layout::from_names(&LayoutNames::from_lookup(|_| None))
|
|
289
|
-
.expect("a keymap should compile");
|
|
290
|
-
let (plan, unreachable) = layout.plan_text("aA.\n");
|
|
291
|
-
assert!(
|
|
292
|
-
unreachable.is_empty(),
|
|
293
|
-
"ASCII should be reachable: {unreachable:?}"
|
|
294
|
-
);
|
|
295
|
-
assert_eq!(plan.len(), 4);
|
|
296
|
-
assert!(!plan[0][0].shift, "lower-case a needs no shift");
|
|
297
|
-
assert!(
|
|
298
|
-
plan[1][0].shift,
|
|
299
|
-
"upper-case A needs shift on any normal layout"
|
|
300
|
-
);
|
|
301
|
-
assert_eq!(plan[0][0].code, plan[1][0].code, "both letters share a key");
|
|
302
|
-
// The plan is evdev, but the real injector expects Moonlight/VK codes.
|
|
303
|
-
// Passing Linux N (49) straight through instead presses VK_1.
|
|
304
|
-
let native: Vec<_> = plan
|
|
305
|
-
.iter()
|
|
306
|
-
.flatten()
|
|
307
|
-
.map(|stroke| crate::input::native_keycode(stroke.code).unwrap())
|
|
308
|
-
.collect();
|
|
309
|
-
assert_eq!(native, [0x41, 0x41, 0xBE, 0x0D]);
|
|
310
|
-
assert_eq!(
|
|
311
|
-
crate::input::native_keycode(plan[1][0].modifiers()[0]).unwrap(),
|
|
312
|
-
0x10
|
|
313
|
-
);
|
|
314
|
-
assert_eq!(
|
|
315
|
-
crate::input::native_keycode(modifier_key("Control").unwrap()).unwrap(),
|
|
316
|
-
0x11
|
|
317
|
-
);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
#[test]
|
|
321
|
-
fn named_keys_and_modifiers_resolve_to_linux_key_codes() {
|
|
322
|
-
assert_eq!(named_key("Enter").unwrap().code, keycode::ENTER);
|
|
323
|
-
assert_eq!(named_key("ArrowLeft").unwrap().code, keycode::LEFT);
|
|
324
|
-
for (name, code) in [
|
|
325
|
-
("Delete", keycode::DELETE), ("Home", keycode::HOME), ("End", keycode::END),
|
|
326
|
-
("PageUp", keycode::PAGE_UP), ("PageDown", keycode::PAGE_DOWN),
|
|
327
|
-
("F1", keycode::F1), ("F2", keycode::F2), ("F3", keycode::F3),
|
|
328
|
-
("F4", keycode::F4), ("F5", keycode::F5), ("F6", keycode::F6),
|
|
329
|
-
("F7", keycode::F7), ("F8", keycode::F8), ("F9", keycode::F9),
|
|
330
|
-
("F10", keycode::F10), ("F11", keycode::F11), ("F12", keycode::F12),
|
|
331
|
-
] {
|
|
332
|
-
assert_eq!(named_key(name).unwrap().code, code);
|
|
333
|
-
assert!(crate::input::native_keycode(code).is_ok(), "{name} must reach inputtino");
|
|
334
|
-
}
|
|
335
|
-
assert_eq!(modifier_key("Control"), Some(keycode::LEFT_CTRL));
|
|
336
|
-
assert_eq!(modifier_key("Alt"), Some(keycode::LEFT_ALT));
|
|
337
|
-
assert_eq!(modifier_key("Super"), Some(keycode::LEFT_META));
|
|
338
|
-
assert!(named_key("Frobnicate").is_none());
|
|
339
|
-
assert!(modifier_key("Hyper").is_none());
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
#[test]
|
|
343
|
-
fn text_this_layout_cannot_produce_is_reported_not_invented() {
|
|
344
|
-
let layout = Layout::from_environment().unwrap();
|
|
345
|
-
let (_, unreachable) = layout.plan_text("\u{1F600}");
|
|
346
|
-
assert_eq!(unreachable, "\u{1F600}");
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
#[test]
|
|
350
|
-
fn a_symbol_only_reachable_beyond_four_levels_is_refused() {
|
|
351
|
-
let names = LayoutNames::from_lookup(|key| match key {
|
|
352
|
-
"XKB_DEFAULT_LAYOUT" => Some(String::from("de")),
|
|
353
|
-
"XKB_DEFAULT_VARIANT" => Some(String::from("neo")),
|
|
354
|
-
_ => None,
|
|
355
|
-
});
|
|
356
|
-
let layout = Layout::from_names(&names).expect("the Neo keymap should compile");
|
|
357
|
-
assert!(
|
|
358
|
-
layout.keystroke_for_char('\u{a3}').is_none(),
|
|
359
|
-
"the pound sign sits on a level this engine cannot press"
|
|
360
|
-
);
|
|
361
|
-
assert!(layout.keystroke_for_char('6').is_some());
|
|
362
|
-
}
|
|
363
|
-
}
|
package/engine/src/keymap_mac.rs
CHANGED
|
@@ -182,28 +182,3 @@ fn us_ansi_key(character: char) -> Option<(i16, bool)> {
|
|
|
182
182
|
_ => return None,
|
|
183
183
|
})
|
|
184
184
|
}
|
|
185
|
-
|
|
186
|
-
#[cfg(test)]
|
|
187
|
-
mod tests {
|
|
188
|
-
use super::*;
|
|
189
|
-
|
|
190
|
-
#[test]
|
|
191
|
-
fn maps_common_ascii_to_ansi_virtual_keys() {
|
|
192
|
-
let layout = Layout::from_environment().unwrap();
|
|
193
|
-
let lower = layout.keystroke_for_char('a').unwrap();
|
|
194
|
-
let upper = layout.keystroke_for_char('A').unwrap();
|
|
195
|
-
assert_eq!(lower.code, 0);
|
|
196
|
-
assert_eq!(upper.code, lower.code);
|
|
197
|
-
assert!(upper.shift);
|
|
198
|
-
assert_eq!(layout.keystroke_for_char('!').unwrap().code, 18);
|
|
199
|
-
assert!(layout.keystroke_for_char('\u{1f600}').is_none());
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
#[test]
|
|
203
|
-
fn resolves_mac_named_keys_and_modifiers() {
|
|
204
|
-
assert_eq!(named_key("Return").unwrap().code, 36);
|
|
205
|
-
assert_eq!(named_key("ArrowLeft").unwrap().code, 123);
|
|
206
|
-
assert_eq!(modifier_key("Command"), Some(55));
|
|
207
|
-
assert!(named_key("Frobnicate").is_none());
|
|
208
|
-
}
|
|
209
|
-
}
|
package/engine/src/mac.rs
CHANGED
|
@@ -717,31 +717,3 @@ pub fn run() -> i32 {
|
|
|
717
717
|
}
|
|
718
718
|
0
|
|
719
719
|
}
|
|
720
|
-
|
|
721
|
-
#[cfg(test)]
|
|
722
|
-
mod tests {
|
|
723
|
-
use super::*;
|
|
724
|
-
|
|
725
|
-
#[cfg(desklink_vpx)]
|
|
726
|
-
#[test]
|
|
727
|
-
fn session_capabilities_are_non_prompting_and_expose_mac_features() {
|
|
728
|
-
let value = crate::session::capabilities();
|
|
729
|
-
assert_eq!(value["protocol"], crate::protocol::PROTOCOL_VERSION);
|
|
730
|
-
assert_eq!(value["platform"], "macos");
|
|
731
|
-
assert_eq!(value["capture"]["mechanism"], "screencapturekit");
|
|
732
|
-
assert_eq!(value["encode"]["codecs"][0], "vp9", "VP9 is always offered");
|
|
733
|
-
// Every Mac this engine runs on has a hardware H.264 encoder.
|
|
734
|
-
assert_eq!(value["encode"]["codecs"][1], "h264");
|
|
735
|
-
assert_eq!(value["encode"]["h264"]["encoder"], "videotoolbox");
|
|
736
|
-
assert!(value["capture"]["displays"].is_array());
|
|
737
|
-
assert!(value["clipboard"]["read"].as_bool().unwrap());
|
|
738
|
-
assert!(matches!(
|
|
739
|
-
value["capture"]["grant"].as_str(),
|
|
740
|
-
Some("granted" | "missing-screen-recording")
|
|
741
|
-
));
|
|
742
|
-
assert!(matches!(
|
|
743
|
-
value["input"]["grant"].as_str(),
|
|
744
|
-
Some("granted" | "missing-accessibility")
|
|
745
|
-
));
|
|
746
|
-
}
|
|
747
|
-
}
|
package/engine/src/peer.rs
CHANGED
|
@@ -1153,6 +1153,7 @@ fn lock<T>(mutex: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
|
|
|
1153
1153
|
#[cfg(test)]
|
|
1154
1154
|
pub(crate) mod tests {
|
|
1155
1155
|
use super::*;
|
|
1156
|
+
use webrtc::media_stream::track_remote::{TrackRemote, TrackRemoteEvent};
|
|
1156
1157
|
|
|
1157
1158
|
#[test]
|
|
1158
1159
|
fn a_standing_queue_is_measured_and_one_frames_burst_is_not() {
|
|
@@ -1589,6 +1590,229 @@ pub(crate) mod tests {
|
|
|
1589
1590
|
.expect("the restart answer is accepted");
|
|
1590
1591
|
}
|
|
1591
1592
|
|
|
1593
|
+
/// A receiver peer that records when each inbound RTP packet arrives and
|
|
1594
|
+
/// reports its own ICE candidates, so two peers can really connect.
|
|
1595
|
+
struct MediaReceiver {
|
|
1596
|
+
candidates: tokio::sync::mpsc::UnboundedSender<RTCIceCandidateInit>,
|
|
1597
|
+
arrivals: Arc<Mutex<Vec<Instant>>>,
|
|
1598
|
+
connected: Arc<AtomicBool>,
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
#[async_trait::async_trait]
|
|
1602
|
+
impl PeerConnectionEventHandler for MediaReceiver {
|
|
1603
|
+
async fn on_ice_candidate(&self, event: RTCPeerConnectionIceEvent) {
|
|
1604
|
+
if let Ok(init) = event.candidate.to_json() {
|
|
1605
|
+
let _ = self.candidates.send(init);
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
async fn on_connection_state_change(&self, state: RTCPeerConnectionState) {
|
|
1610
|
+
self.connected.store(
|
|
1611
|
+
matches!(state, RTCPeerConnectionState::Connected),
|
|
1612
|
+
Ordering::SeqCst,
|
|
1613
|
+
);
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
async fn on_track(&self, track: Arc<dyn TrackRemote>) {
|
|
1617
|
+
let arrivals = Arc::clone(&self.arrivals);
|
|
1618
|
+
tokio::spawn(async move {
|
|
1619
|
+
while let Some(event) = track.poll().await {
|
|
1620
|
+
if matches!(event, TrackRemoteEvent::OnRtpPacket(_)) {
|
|
1621
|
+
arrivals.lock().unwrap().push(Instant::now());
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
});
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
async fn media_receiver(receiver: Arc<MediaReceiver>) -> Arc<dyn PeerConnection> {
|
|
1629
|
+
let mut media_engine = MediaEngine::default();
|
|
1630
|
+
media_engine
|
|
1631
|
+
.register_codec(vp9_codec(), RtpCodecKind::Video)
|
|
1632
|
+
.expect("the codec registers");
|
|
1633
|
+
let registry = register_default_interceptors(Registry::new(), &mut media_engine)
|
|
1634
|
+
.expect("interceptors register");
|
|
1635
|
+
let peer = PeerConnectionBuilder::<std::net::SocketAddr>::new()
|
|
1636
|
+
.with_media_engine(media_engine)
|
|
1637
|
+
.with_interceptor_registry(registry)
|
|
1638
|
+
.with_runtime(default_runtime().expect("a runtime"))
|
|
1639
|
+
.with_handler(receiver)
|
|
1640
|
+
.with_udp_addrs(vec![std::net::SocketAddr::from(([0, 0, 0, 0], 0))])
|
|
1641
|
+
.build()
|
|
1642
|
+
.await
|
|
1643
|
+
.expect("a receiving peer");
|
|
1644
|
+
Arc::new(peer)
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
/// A restart offer/answer round must not interrupt media already flowing on
|
|
1648
|
+
/// the selected pair: the picture keeps advancing (no gap over 200 ms) while
|
|
1649
|
+
/// the old pair stays reachable, and the ICE generation still turns over.
|
|
1650
|
+
#[tokio::test]
|
|
1651
|
+
async fn a_restart_keeps_media_flowing_on_the_selected_pair_until_the_new_one_nominates() {
|
|
1652
|
+
let (events, mut events_rx) = tokio::sync::mpsc::unbounded_channel();
|
|
1653
|
+
let (engine, offer) = VideoPeer::offer(transport(), events, VideoCodec::Vp9)
|
|
1654
|
+
.await
|
|
1655
|
+
.expect("an offering peer");
|
|
1656
|
+
let engine = Arc::new(engine);
|
|
1657
|
+
|
|
1658
|
+
let arrivals: Arc<Mutex<Vec<Instant>>> = Arc::new(Mutex::new(Vec::new()));
|
|
1659
|
+
let connected = Arc::new(AtomicBool::new(false));
|
|
1660
|
+
let (client_candidates_tx, mut client_candidates_rx) =
|
|
1661
|
+
tokio::sync::mpsc::unbounded_channel();
|
|
1662
|
+
let receiver = Arc::new(MediaReceiver {
|
|
1663
|
+
candidates: client_candidates_tx,
|
|
1664
|
+
arrivals: Arc::clone(&arrivals),
|
|
1665
|
+
connected: Arc::clone(&connected),
|
|
1666
|
+
});
|
|
1667
|
+
let client = media_receiver(Arc::clone(&receiver)).await;
|
|
1668
|
+
|
|
1669
|
+
client
|
|
1670
|
+
.set_remote_description(RTCSessionDescription::offer(offer.clone()).expect("an offer"))
|
|
1671
|
+
.await
|
|
1672
|
+
.expect("the offer applies");
|
|
1673
|
+
let answer = client.create_answer(None).await.expect("an answer");
|
|
1674
|
+
client
|
|
1675
|
+
.set_local_description(answer.clone())
|
|
1676
|
+
.await
|
|
1677
|
+
.expect("a local answer");
|
|
1678
|
+
engine
|
|
1679
|
+
.accept_answer(answer.sdp.clone())
|
|
1680
|
+
.await
|
|
1681
|
+
.expect("the answer is accepted");
|
|
1682
|
+
|
|
1683
|
+
// The engine's candidates reach the client.
|
|
1684
|
+
let client_for_engine_candidates = Arc::clone(&client);
|
|
1685
|
+
tokio::spawn(async move {
|
|
1686
|
+
while let Some(event) = events_rx.recv().await {
|
|
1687
|
+
if let PeerEvent::Candidate {
|
|
1688
|
+
candidate,
|
|
1689
|
+
sdp_mid,
|
|
1690
|
+
sdp_m_line_index,
|
|
1691
|
+
} = event
|
|
1692
|
+
{
|
|
1693
|
+
let _ = client_for_engine_candidates
|
|
1694
|
+
.add_ice_candidate(RTCIceCandidateInit {
|
|
1695
|
+
candidate,
|
|
1696
|
+
sdp_mid,
|
|
1697
|
+
sdp_mline_index: sdp_m_line_index,
|
|
1698
|
+
username_fragment: None,
|
|
1699
|
+
url: None,
|
|
1700
|
+
})
|
|
1701
|
+
.await;
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
});
|
|
1705
|
+
// The client's candidates reach the engine.
|
|
1706
|
+
let engine_for_client_candidates = Arc::clone(&engine);
|
|
1707
|
+
tokio::spawn(async move {
|
|
1708
|
+
while let Some(init) = client_candidates_rx.recv().await {
|
|
1709
|
+
let _ = engine_for_client_candidates
|
|
1710
|
+
.add_candidate(init.candidate, init.sdp_mid, init.sdp_mline_index)
|
|
1711
|
+
.await;
|
|
1712
|
+
}
|
|
1713
|
+
});
|
|
1714
|
+
|
|
1715
|
+
// Wait for the ICE path to come up on both ends.
|
|
1716
|
+
let deadline = Instant::now() + Duration::from_secs(5);
|
|
1717
|
+
while !(engine.is_connected() && connected.load(Ordering::SeqCst)) {
|
|
1718
|
+
assert!(Instant::now() < deadline, "the peers never connected");
|
|
1719
|
+
tokio::time::sleep(Duration::from_millis(10)).await;
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
// Any bytes the VP9 packetizer turns into one access unit.
|
|
1723
|
+
let frame: Vec<u8> = (0..200u32).map(|i| (i % 251) as u8).collect();
|
|
1724
|
+
let sending = Arc::new(AtomicBool::new(true));
|
|
1725
|
+
let engine_for_sending = Arc::clone(&engine);
|
|
1726
|
+
let sending_for_task = Arc::clone(&sending);
|
|
1727
|
+
let frame_for_task = frame.clone();
|
|
1728
|
+
tokio::spawn(async move {
|
|
1729
|
+
while sending_for_task.load(Ordering::SeqCst) {
|
|
1730
|
+
let _ = engine_for_sending
|
|
1731
|
+
.send_frame(&frame_for_task, true, Instant::now())
|
|
1732
|
+
.await;
|
|
1733
|
+
tokio::time::sleep(Duration::from_millis(5)).await;
|
|
1734
|
+
}
|
|
1735
|
+
});
|
|
1736
|
+
|
|
1737
|
+
// Let the picture flow, then take a clean baseline.
|
|
1738
|
+
tokio::time::sleep(Duration::from_millis(300)).await;
|
|
1739
|
+
arrivals.lock().unwrap().clear();
|
|
1740
|
+
tokio::time::sleep(Duration::from_millis(200)).await;
|
|
1741
|
+
let flowing = arrivals.lock().unwrap().len();
|
|
1742
|
+
assert!(
|
|
1743
|
+
flowing >= 5,
|
|
1744
|
+
"media was not flowing before the restart: {flowing} packets"
|
|
1745
|
+
);
|
|
1746
|
+
|
|
1747
|
+
// The restart a client drives when its carrier comes back on a roam.
|
|
1748
|
+
let restart = engine.restart_ice().await.expect("a restart offer");
|
|
1749
|
+
let ufrag = |sdp: &str| {
|
|
1750
|
+
sdp.lines()
|
|
1751
|
+
.find_map(|line| line.strip_prefix("a=ice-ufrag:"))
|
|
1752
|
+
.expect("ICE credentials")
|
|
1753
|
+
.to_owned()
|
|
1754
|
+
};
|
|
1755
|
+
assert_ne!(
|
|
1756
|
+
ufrag(&restart),
|
|
1757
|
+
ufrag(&offer),
|
|
1758
|
+
"the restart must turn the ICE generation over"
|
|
1759
|
+
);
|
|
1760
|
+
|
|
1761
|
+
let restart_at = Instant::now();
|
|
1762
|
+
arrivals.lock().unwrap().clear();
|
|
1763
|
+
|
|
1764
|
+
// The restart offer crosses the consumer's signaling channel before the
|
|
1765
|
+
// client answers, and the engine applies the restart at
|
|
1766
|
+
// `set_local_description` — inside `restart_ice` above, before the offer
|
|
1767
|
+
// has even left. It must keep feeding media on the old pair for that
|
|
1768
|
+
// whole round trip, not only until its own teardown. A real channel
|
|
1769
|
+
// costs more than a loopback hop, so hold this window for the bar's
|
|
1770
|
+
// length: a teardown shows up here, a preserved pair does not.
|
|
1771
|
+
tokio::time::sleep(Duration::from_millis(300)).await;
|
|
1772
|
+
|
|
1773
|
+
client
|
|
1774
|
+
.set_remote_description(RTCSessionDescription::offer(restart).expect("a restart offer"))
|
|
1775
|
+
.await
|
|
1776
|
+
.expect("the restart offer applies");
|
|
1777
|
+
let reanswer = client.create_answer(None).await.expect("an answer");
|
|
1778
|
+
client
|
|
1779
|
+
.set_local_description(reanswer.clone())
|
|
1780
|
+
.await
|
|
1781
|
+
.expect("a local answer");
|
|
1782
|
+
engine
|
|
1783
|
+
.accept_answer(reanswer.sdp)
|
|
1784
|
+
.await
|
|
1785
|
+
.expect("the restart answer is accepted");
|
|
1786
|
+
|
|
1787
|
+
// Keep the picture advancing across and after the restart.
|
|
1788
|
+
tokio::time::sleep(Duration::from_millis(800)).await;
|
|
1789
|
+
sending.store(false, Ordering::SeqCst);
|
|
1790
|
+
|
|
1791
|
+
let stamps = arrivals.lock().unwrap().clone();
|
|
1792
|
+
assert!(
|
|
1793
|
+
!stamps.is_empty(),
|
|
1794
|
+
"no media arrived at all after the restart"
|
|
1795
|
+
);
|
|
1796
|
+
let mut previous = restart_at;
|
|
1797
|
+
let mut worst = Duration::ZERO;
|
|
1798
|
+
for stamp in &stamps {
|
|
1799
|
+
worst = worst.max(stamp.duration_since(previous));
|
|
1800
|
+
previous = *stamp;
|
|
1801
|
+
}
|
|
1802
|
+
eprintln!(
|
|
1803
|
+
"restart: {} packets after the offer, worst media gap {worst:?}",
|
|
1804
|
+
stamps.len()
|
|
1805
|
+
);
|
|
1806
|
+
assert!(
|
|
1807
|
+
worst < Duration::from_millis(200),
|
|
1808
|
+
"media stalled for {worst:?} through the restart: {:?}",
|
|
1809
|
+
stamps
|
|
1810
|
+
);
|
|
1811
|
+
|
|
1812
|
+
engine.close().await;
|
|
1813
|
+
let _ = client.close().await;
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1592
1816
|
#[tokio::test]
|
|
1593
1817
|
async fn a_forwarded_client_is_offered_ice_over_tcp_on_the_loopback() {
|
|
1594
1818
|
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
|
-
}
|