@desklink/host 0.1.0 → 0.2.0
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/NOTICE +3 -0
- package/README.md +72 -16
- package/bin/desklink-host.mjs +14 -3
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js +5 -1
- package/dist/bridge.js.map +1 -1
- package/dist/engineProcess.d.ts.map +1 -1
- package/dist/engineProcess.js +17 -5
- package/dist/engineProcess.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/keeper.d.ts +52 -0
- package/dist/keeper.d.ts.map +1 -0
- package/dist/keeper.js +101 -0
- package/dist/keeper.js.map +1 -0
- package/dist/protocol.d.ts +42 -5
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +1 -1
- package/dist/protocol.js.map +1 -1
- package/dist/resolveEngine.d.ts +3 -2
- package/dist/resolveEngine.d.ts.map +1 -1
- package/dist/resolveEngine.js +20 -5
- package/dist/resolveEngine.js.map +1 -1
- package/docs/BOUNDARY.md +11 -6
- package/docs/PROTOCOL.md +297 -33
- package/engine/Cargo.lock +196 -1
- package/engine/Cargo.toml +26 -5
- package/engine/build.rs +98 -23
- package/engine/native/agent_overlay_mac.m +133 -0
- package/engine/native/agent_overlay_wayland.c +192 -0
- package/engine/native/agent_overlay_x11.c +103 -0
- package/engine/native/mac_stream.mm +188 -0
- package/engine/src/capture.rs +169 -19
- package/engine/src/capture_mac.rs +145 -0
- package/engine/src/clipboard_mac.rs +182 -0
- package/engine/src/convert.rs +57 -1
- package/engine/src/indicator.rs +162 -0
- package/engine/src/input.rs +12 -0
- package/engine/src/input_mac.rs +312 -0
- package/engine/src/keeper.rs +622 -0
- package/engine/src/keymap.rs +25 -0
- package/engine/src/keymap_mac.rs +209 -0
- package/engine/src/mac.rs +722 -0
- package/engine/src/main.rs +158 -2
- package/engine/src/peer.rs +429 -22
- package/engine/src/portal.rs +4 -0
- package/engine/src/portal_mac.rs +68 -0
- package/engine/src/protocol.rs +186 -3
- package/engine/src/session.rs +1307 -100
- package/engine/src/x11.rs +23 -3
- package/engine/src/x11_mac.rs +40 -0
- package/examples/reference-client.html +33 -4
- package/package.json +2 -2
package/engine/src/session.rs
CHANGED
|
@@ -11,9 +11,11 @@ use crate::convert::{fit, I420};
|
|
|
11
11
|
use crate::encoder::Encoder;
|
|
12
12
|
use crate::input::{Button, HeldState, InputDevices};
|
|
13
13
|
use crate::keymap::{self, Layout};
|
|
14
|
-
use crate::peer::{PeerEvent, TransportOptions, VideoPeer};
|
|
14
|
+
use crate::peer::{h264_profile_level_id, PeerEvent, TransportOptions, VideoCodec, VideoPeer};
|
|
15
15
|
use crate::portal::{self, SelectedSource};
|
|
16
|
-
use crate::protocol::{
|
|
16
|
+
use crate::protocol::{
|
|
17
|
+
ControlMessage, ControlReply, EncodedCodec, Permission, PointerPhase, SourceRequest,
|
|
18
|
+
};
|
|
17
19
|
use crate::x11::X11Desktop;
|
|
18
20
|
use anyhow::Result;
|
|
19
21
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
@@ -59,13 +61,88 @@ pub struct Metrics {
|
|
|
59
61
|
pub target_kbps: u32,
|
|
60
62
|
pub input_applied: u64,
|
|
61
63
|
pub input_rejected: u64,
|
|
64
|
+
/// Access units the consumer fed to an encoded source.
|
|
65
|
+
pub fed_frames: u64,
|
|
66
|
+
/// Control messages forwarded back to the consumer that owns the device,
|
|
67
|
+
/// instead of being applied to a desktop this engine holds.
|
|
68
|
+
pub input_forwarded: u64,
|
|
62
69
|
}
|
|
63
70
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
71
|
+
fn tile_hashes(raw: &[u8], width: usize, height: usize) -> Vec<u64> {
|
|
72
|
+
use std::hash::{Hash, Hasher};
|
|
73
|
+
let mut hashes = Vec::new();
|
|
74
|
+
for y in (0..height).step_by(32) {
|
|
75
|
+
for x in (0..width).step_by(32) {
|
|
76
|
+
let mut hash = std::collections::hash_map::DefaultHasher::new();
|
|
77
|
+
for row in y..(y + 32).min(height) {
|
|
78
|
+
raw[(row * width + x) * 4..(row * width + (x + 32).min(width)) * 4].hash(&mut hash);
|
|
79
|
+
}
|
|
80
|
+
hashes.push(hash.finish());
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
hashes
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/// Group changed 32-pixel tiles without merging unrelated regions.
|
|
87
|
+
fn dirty_regions(hashes: &[u64], previous: &[u64], width: usize, height: usize) -> Vec<[usize; 4]> {
|
|
88
|
+
if hashes.len() != previous.len() {
|
|
89
|
+
return vec![[0, 0, width, height]];
|
|
90
|
+
}
|
|
91
|
+
let columns = width.div_ceil(32);
|
|
92
|
+
let mut dirty: Vec<bool> = hashes
|
|
93
|
+
.iter()
|
|
94
|
+
.zip(previous)
|
|
95
|
+
.map(|(now, before)| now != before)
|
|
96
|
+
.collect();
|
|
97
|
+
let mut regions = Vec::new();
|
|
98
|
+
for index in 0..dirty.len() {
|
|
99
|
+
if !dirty[index] {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
dirty[index] = false;
|
|
103
|
+
let mut stack = vec![index];
|
|
104
|
+
let (mut left, mut top, mut right, mut bottom) = (
|
|
105
|
+
index % columns,
|
|
106
|
+
index / columns,
|
|
107
|
+
index % columns,
|
|
108
|
+
index / columns,
|
|
109
|
+
);
|
|
110
|
+
while let Some(tile) = stack.pop() {
|
|
111
|
+
let col = tile % columns;
|
|
112
|
+
let row = tile / columns;
|
|
113
|
+
left = left.min(col);
|
|
114
|
+
right = right.max(col);
|
|
115
|
+
top = top.min(row);
|
|
116
|
+
bottom = bottom.max(row);
|
|
117
|
+
for neighbor in [
|
|
118
|
+
tile.checked_sub(columns),
|
|
119
|
+
(row + 1 < height.div_ceil(32)).then_some(tile + columns),
|
|
120
|
+
(col > 0).then(|| tile - 1),
|
|
121
|
+
(col + 1 < columns).then_some(tile + 1),
|
|
122
|
+
]
|
|
123
|
+
.into_iter()
|
|
124
|
+
.flatten()
|
|
125
|
+
{
|
|
126
|
+
if dirty[neighbor] {
|
|
127
|
+
dirty[neighbor] = false;
|
|
128
|
+
stack.push(neighbor);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
let x = left * 32;
|
|
133
|
+
let y = top * 32;
|
|
134
|
+
regions.push([
|
|
135
|
+
x,
|
|
136
|
+
y,
|
|
137
|
+
((right + 1) * 32).min(width) - x,
|
|
138
|
+
((bottom + 1) * 32).min(height) - y,
|
|
139
|
+
]);
|
|
140
|
+
}
|
|
141
|
+
regions
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/// Dropping either variant stops its capture: the portal releases consent and
|
|
145
|
+
/// the X11 variant stops reading its server.
|
|
69
146
|
enum FrameSource {
|
|
70
147
|
Portal(Capture),
|
|
71
148
|
X11(X11Capture),
|
|
@@ -103,6 +180,7 @@ fn select_x11(
|
|
|
103
180
|
max_fps: u32,
|
|
104
181
|
metrics: Arc<Mutex<Metrics>>,
|
|
105
182
|
sink: capture::FrameSink,
|
|
183
|
+
on_stop: Box<dyn Fn(String) + Send>,
|
|
106
184
|
) -> Result<Selected> {
|
|
107
185
|
let desktop = Arc::new(Mutex::new(X11Desktop::connect(display)?));
|
|
108
186
|
let (width, height) = {
|
|
@@ -137,17 +215,17 @@ fn select_x11(
|
|
|
137
215
|
let started = Instant::now();
|
|
138
216
|
let frame = {
|
|
139
217
|
let mut desktop = lock(&desktop);
|
|
140
|
-
desktop.
|
|
218
|
+
desktop.capture_with_pixels(max_width, max_height)
|
|
141
219
|
};
|
|
142
220
|
match frame {
|
|
143
|
-
Ok(frame) => {
|
|
221
|
+
Ok((frame, raw)) => {
|
|
144
222
|
use std::hash::{Hash, Hasher};
|
|
145
223
|
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
|
146
|
-
|
|
224
|
+
raw.hash(&mut hasher);
|
|
147
225
|
let hash = hasher.finish();
|
|
148
226
|
if last_hash != Some(hash) {
|
|
149
227
|
last_hash = Some(hash);
|
|
150
|
-
sink(frame, sequence);
|
|
228
|
+
sink(frame, sequence, raw);
|
|
151
229
|
sequence += 1;
|
|
152
230
|
}
|
|
153
231
|
}
|
|
@@ -158,6 +236,7 @@ fn select_x11(
|
|
|
158
236
|
// A display that has gone away is not recoverable by
|
|
159
237
|
// retrying, so stop rather than spin on the error.
|
|
160
238
|
if error.to_string().contains("the X11 connection dropped") {
|
|
239
|
+
on_stop(format!("X11 capture stopped: {error}"));
|
|
161
240
|
break;
|
|
162
241
|
}
|
|
163
242
|
}
|
|
@@ -312,6 +391,19 @@ impl InputTarget {
|
|
|
312
391
|
Ok(())
|
|
313
392
|
}
|
|
314
393
|
|
|
394
|
+
#[cfg(target_os = "macos")]
|
|
395
|
+
fn unicode_text(&mut self, text: &str) -> Result<()> {
|
|
396
|
+
match &mut self.applier {
|
|
397
|
+
#[cfg(target_os = "macos")]
|
|
398
|
+
Applier::Uinput(devices) => devices.unicode_text(text),
|
|
399
|
+
#[cfg(target_os = "linux")]
|
|
400
|
+
Applier::Uinput(_) => anyhow::bail!("Unicode text input is unavailable on Linux"),
|
|
401
|
+
Applier::X11(_) => anyhow::bail!("Unicode text input is unavailable for X11"),
|
|
402
|
+
#[cfg(test)]
|
|
403
|
+
Applier::Recording(_) => Ok(()),
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
315
407
|
fn chord(&mut self, code: i16, modifiers: Vec<i16>, down: bool) -> Result<()> {
|
|
316
408
|
if down {
|
|
317
409
|
if self.chord_keys.iter().any(|(held, _)| *held == code) {
|
|
@@ -375,7 +467,7 @@ fn x11_button(button: Button) -> u8 {
|
|
|
375
467
|
}
|
|
376
468
|
}
|
|
377
469
|
|
|
378
|
-
fn lock<T>(mutex: &
|
|
470
|
+
fn lock<T>(mutex: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
|
|
379
471
|
// A poisoned lock means a previous input call panicked while applying; the
|
|
380
472
|
// desktop is then in unknown state, so the session is closed rather than
|
|
381
473
|
// continuing to drive it.
|
|
@@ -385,6 +477,7 @@ fn lock<T>(mutex: &Arc<Mutex<T>>) -> std::sync::MutexGuard<'_, T> {
|
|
|
385
477
|
}
|
|
386
478
|
|
|
387
479
|
/// Why a session could not do what was asked, in the protocol's own vocabulary.
|
|
480
|
+
#[derive(Debug)]
|
|
388
481
|
pub struct SessionError {
|
|
389
482
|
pub code: &'static str,
|
|
390
483
|
pub message: String,
|
|
@@ -412,6 +505,7 @@ pub struct OpenRequest {
|
|
|
412
505
|
pub ttl: Option<Duration>,
|
|
413
506
|
/// Offer a passive ICE-TCP candidate on the loopback, for a forwarded client.
|
|
414
507
|
pub loopback_tcp: bool,
|
|
508
|
+
pub agent_indicator: bool,
|
|
415
509
|
}
|
|
416
510
|
|
|
417
511
|
/// Events a session raises for its consumer.
|
|
@@ -432,7 +526,26 @@ pub enum SessionEvent {
|
|
|
432
526
|
transport: String,
|
|
433
527
|
first_frame: bool,
|
|
434
528
|
},
|
|
529
|
+
Frame {
|
|
530
|
+
seq: u64,
|
|
531
|
+
damage: Vec<[usize; 4]>,
|
|
532
|
+
},
|
|
533
|
+
/// One control-channel message from the client, for a source whose device
|
|
534
|
+
/// the consumer drives. The engine applied nothing locally.
|
|
535
|
+
Input {
|
|
536
|
+
input: serde_json::Value,
|
|
537
|
+
},
|
|
538
|
+
/// The far end needs a fresh reference frame and only the consumer can make
|
|
539
|
+
/// one: the encoder for this track is on its side. Raised by an RTCP PLI or
|
|
540
|
+
/// FIR, and once when the transport connects, because everything sent before
|
|
541
|
+
/// that was dropped.
|
|
542
|
+
KeyframeRequest {
|
|
543
|
+
generation: u64,
|
|
544
|
+
},
|
|
435
545
|
RestoreToken(String),
|
|
546
|
+
CaptureStopped {
|
|
547
|
+
reason: String,
|
|
548
|
+
},
|
|
436
549
|
Revoked {
|
|
437
550
|
reason: String,
|
|
438
551
|
},
|
|
@@ -447,6 +560,8 @@ pub struct Notice {
|
|
|
447
560
|
pub event: SessionEvent,
|
|
448
561
|
}
|
|
449
562
|
|
|
563
|
+
type FrameSnapshot = (u64, Instant, usize, usize, Vec<u8>, Vec<u64>, Vec<u64>);
|
|
564
|
+
|
|
450
565
|
/// Everything a session's background tasks need, shared rather than borrowed so
|
|
451
566
|
/// the consumer can own the `Session` handle while the pipeline runs.
|
|
452
567
|
struct Inner {
|
|
@@ -456,11 +571,21 @@ struct Inner {
|
|
|
456
571
|
source: SelectedSource,
|
|
457
572
|
geometry: serde_json::Value,
|
|
458
573
|
metrics: Arc<Mutex<Metrics>>,
|
|
459
|
-
|
|
460
|
-
|
|
574
|
+
latest: Arc<Mutex<Option<FrameSnapshot>>>,
|
|
575
|
+
capture_error: Arc<Mutex<Option<String>>>,
|
|
576
|
+
frame_changes: tokio::sync::watch::Receiver<u64>,
|
|
577
|
+
/// Built when the codec is known: with `session.open` for a captured
|
|
578
|
+
/// desktop, and with the first fed access unit for an encoded source, whose
|
|
579
|
+
/// offer has to name the profile the stream's own SPS declares.
|
|
580
|
+
peer: Mutex<Option<Arc<VideoPeer>>>,
|
|
581
|
+
/// Absent for an encoded source: the consumer is the encoder.
|
|
582
|
+
encoder: Mutex<Option<Encoder>>,
|
|
461
583
|
input: Mutex<Option<InputTarget>>,
|
|
584
|
+
indicator: Mutex<Option<crate::indicator::Indicator>>,
|
|
462
585
|
capture: Mutex<Option<FrameSource>>,
|
|
463
|
-
|
|
586
|
+
/// Absent for an encoded source: there is no local keyboard to translate a
|
|
587
|
+
/// character through, because the client's keys are forwarded instead.
|
|
588
|
+
layout: Mutex<Option<Layout>>,
|
|
464
589
|
last_seq: Mutex<u64>,
|
|
465
590
|
control_open: AtomicBool,
|
|
466
591
|
closed: AtomicBool,
|
|
@@ -468,18 +593,47 @@ struct Inner {
|
|
|
468
593
|
/// from the lease as well as from the consumer.
|
|
469
594
|
pipeline: Arc<AtomicBool>,
|
|
470
595
|
events: tokio_mpsc::UnboundedSender<Notice>,
|
|
596
|
+
/// Present only for an encoded source.
|
|
597
|
+
encoded: Mutex<Option<Encoded>>,
|
|
471
598
|
}
|
|
472
599
|
|
|
600
|
+
/// A source whose video arrives already encoded.
|
|
601
|
+
struct Encoded {
|
|
602
|
+
/// What the consumer said it would feed. Only H.264 exists today; the offer
|
|
603
|
+
/// is built from this rather than assumed, so a second codec is one arm here.
|
|
604
|
+
codec: EncodedCodec,
|
|
605
|
+
/// The transport settings the consumer chose in `session.open`, kept until
|
|
606
|
+
/// the peer that uses them can be built.
|
|
607
|
+
transport: TransportOptions,
|
|
608
|
+
/// Where the peer's events go once it exists.
|
|
609
|
+
peer_events: tokio_mpsc::UnboundedSender<PeerEvent>,
|
|
610
|
+
/// Access units waiting to be packetized.
|
|
611
|
+
feed: tokio_mpsc::Sender<FedAccessUnit>,
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/// One access unit a consumer fed, with the keyframe flag it declared.
|
|
615
|
+
struct FedAccessUnit {
|
|
616
|
+
keyframe: bool,
|
|
617
|
+
data: Vec<u8>,
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/// How many fed access units may wait. A live stream drops the newest rather
|
|
621
|
+
/// than queue: the packetizer is the only consumer, and a backlog of
|
|
622
|
+
/// milliseconds-old pictures is worse than a gap the receiver asks to repair.
|
|
623
|
+
const FEED_BACKLOG: usize = 8;
|
|
624
|
+
|
|
473
625
|
pub struct Session {
|
|
474
626
|
inner: Arc<Inner>,
|
|
475
627
|
}
|
|
476
628
|
|
|
629
|
+
#[cfg(target_os = "linux")]
|
|
477
630
|
fn wayland_clipboard_available() -> bool {
|
|
478
631
|
std::env::var("XDG_SESSION_TYPE").as_deref() == Ok("wayland")
|
|
479
632
|
&& std::env::var_os("WAYLAND_DISPLAY").is_some()
|
|
480
633
|
}
|
|
481
634
|
|
|
482
635
|
/// What the engine can actually do on this machine right now.
|
|
636
|
+
#[cfg(target_os = "linux")]
|
|
483
637
|
pub fn capabilities() -> serde_json::Value {
|
|
484
638
|
let session_kind = if std::env::var_os("WAYLAND_DISPLAY").is_some() {
|
|
485
639
|
"wayland"
|
|
@@ -534,6 +688,65 @@ pub fn capabilities() -> serde_json::Value {
|
|
|
534
688
|
},
|
|
535
689
|
"clipboard": { "read": clipboard, "write": clipboard && clipboard::writer_available(), "mime": ["text/plain;charset=utf-8"],
|
|
536
690
|
"maxBytes": clipboard::MAX_CLIPBOARD_BYTES },
|
|
691
|
+
// Video this engine does not capture or encode but can carry, so a
|
|
692
|
+
// consumer can tell an old engine from one that takes `session.feed`
|
|
693
|
+
// without guessing from a version string.
|
|
694
|
+
"encoded": { "codecs": ["h264"] },
|
|
695
|
+
})
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
#[cfg(target_os = "macos")]
|
|
699
|
+
fn wayland_clipboard_available() -> bool {
|
|
700
|
+
true
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
#[cfg(target_os = "macos")]
|
|
704
|
+
pub fn capabilities() -> serde_json::Value {
|
|
705
|
+
let capture_granted = unsafe { crate::mac::CGPreflightScreenCaptureAccess() };
|
|
706
|
+
let (input, input_grant, unavailable) = match crate::input::probe() {
|
|
707
|
+
Ok(()) => (true, "granted", serde_json::Value::Null),
|
|
708
|
+
Err(error) => (
|
|
709
|
+
false,
|
|
710
|
+
"missing-accessibility",
|
|
711
|
+
serde_json::json!({
|
|
712
|
+
"reason": error.reason,
|
|
713
|
+
"remedy": error.remedy,
|
|
714
|
+
}),
|
|
715
|
+
),
|
|
716
|
+
};
|
|
717
|
+
serde_json::json!({
|
|
718
|
+
"protocol": crate::protocol::PROTOCOL_VERSION,
|
|
719
|
+
"engine": format!("desklink-host/{}", env!("CARGO_PKG_VERSION")),
|
|
720
|
+
"platform": "macos",
|
|
721
|
+
"session": { "kind": "quartz", "on_console": crate::mac::on_console() },
|
|
722
|
+
"x11": { "available": false, "size": null },
|
|
723
|
+
"capture": {
|
|
724
|
+
"mechanism": "screencapturekit",
|
|
725
|
+
"backends": ["screencapturekit"],
|
|
726
|
+
"formats": ["bgra"],
|
|
727
|
+
"cursor": "embedded",
|
|
728
|
+
"audio": false,
|
|
729
|
+
"displays": crate::mac::displays(),
|
|
730
|
+
"grant": if capture_granted { "granted" } else { "missing-screen-recording" },
|
|
731
|
+
"unavailable_reason": if capture_granted { serde_json::Value::Null } else { serde_json::json!({
|
|
732
|
+
"reason": format!("Screen Recording permission has not been granted to {}.", crate::mac::tcc_responsible_app_name()),
|
|
733
|
+
"remedy": format!("Allow {} in System Settings › Privacy & Security › Screen & System Audio Recording, then reconnect.", crate::mac::tcc_responsible_app_name())
|
|
734
|
+
}) }
|
|
735
|
+
},
|
|
736
|
+
"encode": { "codecs": ["vp9"], "hardware": false },
|
|
737
|
+
"input": {
|
|
738
|
+
"mechanism": "quartz-cgevent",
|
|
739
|
+
"pointer": input,
|
|
740
|
+
"wheel": input,
|
|
741
|
+
"keyboard": input,
|
|
742
|
+
"text": ["unicode", "us-ansi-keymap"],
|
|
743
|
+
"layout": keymap::LayoutNames::from_environment().identity(),
|
|
744
|
+
"unavailable_reason": unavailable,
|
|
745
|
+
"grant": input_grant,
|
|
746
|
+
},
|
|
747
|
+
"clipboard": { "read": true, "write": true, "mime": ["text/plain;charset=utf-8"],
|
|
748
|
+
"maxBytes": clipboard::MAX_CLIPBOARD_BYTES },
|
|
749
|
+
"encoded": { "codecs": ["h264"] },
|
|
537
750
|
})
|
|
538
751
|
}
|
|
539
752
|
|
|
@@ -542,27 +755,114 @@ impl Session {
|
|
|
542
755
|
request: OpenRequest,
|
|
543
756
|
events: tokio_mpsc::UnboundedSender<Notice>,
|
|
544
757
|
) -> std::result::Result<Self, SessionError> {
|
|
758
|
+
if !request.permissions.contains(&Permission::View) {
|
|
759
|
+
return Err(SessionError::new(
|
|
760
|
+
"permission",
|
|
761
|
+
"session.open requires the view permission",
|
|
762
|
+
));
|
|
763
|
+
}
|
|
545
764
|
let id = opaque_id();
|
|
546
765
|
let wants_control = request.permissions.contains(&Permission::Control);
|
|
547
766
|
let wants_x11 = matches!(request.source, Some(SourceRequest::X11 { .. }));
|
|
767
|
+
let max_fps = request.max_fps.clamp(1, 60);
|
|
768
|
+
// A source the consumer feeds needs no capture backend, no encoder and
|
|
769
|
+
// no local input device: the device the client drives is the consumer's,
|
|
770
|
+
// and the offer has to wait for the stream's own SPS.
|
|
771
|
+
if let Some(SourceRequest::Encoded {
|
|
772
|
+
codec,
|
|
773
|
+
width,
|
|
774
|
+
height,
|
|
775
|
+
}) = request.source.clone()
|
|
776
|
+
{
|
|
777
|
+
return Self::open_encoded(request, codec, width, height, events).await;
|
|
778
|
+
}
|
|
548
779
|
// Which desktop decides which input path is even available: an X display
|
|
549
780
|
// takes XTest, which cannot reach any other session, while a portal
|
|
550
781
|
// desktop needs kernel input access.
|
|
551
782
|
if wants_control && !wants_x11 {
|
|
552
783
|
// Refuse up front rather than presenting a control surface that
|
|
553
784
|
// silently does nothing.
|
|
554
|
-
if
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
785
|
+
if crate::input::probe().is_err() {
|
|
786
|
+
#[cfg(target_os = "macos")]
|
|
787
|
+
let _ = crate::input::request_access();
|
|
788
|
+
if let Err(unavailable) = crate::input::probe() {
|
|
789
|
+
return Err(SessionError::new(
|
|
790
|
+
"input-unavailable",
|
|
791
|
+
format!("{}; {}", unavailable.reason, unavailable.remedy),
|
|
792
|
+
));
|
|
793
|
+
}
|
|
559
794
|
}
|
|
560
795
|
}
|
|
561
796
|
|
|
562
797
|
let metrics = Arc::new(Mutex::new(Metrics::default()));
|
|
798
|
+
let mut indicator = None;
|
|
563
799
|
let (frame_tx, frame_rx) = latest_frame();
|
|
800
|
+
let latest = Arc::new(Mutex::new(None::<FrameSnapshot>));
|
|
801
|
+
let observed = latest.clone();
|
|
802
|
+
let frame_sequence = Arc::new(std::sync::atomic::AtomicU64::new(0));
|
|
803
|
+
let capture_error = Arc::new(Mutex::new(None::<String>));
|
|
804
|
+
let stopped_error = capture_error.clone();
|
|
805
|
+
let stopped_latest = latest.clone();
|
|
806
|
+
let stopped_events = events.clone();
|
|
807
|
+
let stopped_id = id.clone();
|
|
808
|
+
let (frame_tx_signal, frame_changes) = tokio::sync::watch::channel(0u64);
|
|
564
809
|
let captured = metrics.clone();
|
|
565
|
-
let
|
|
810
|
+
let stop_signal = frame_tx_signal.clone();
|
|
811
|
+
let on_capture_status = move |running: bool, reason: String| {
|
|
812
|
+
if let Ok(mut error) = stopped_error.lock() {
|
|
813
|
+
*error = if running { None } else { Some(reason.clone()) };
|
|
814
|
+
}
|
|
815
|
+
if let Ok(mut frame) = stopped_latest.lock() {
|
|
816
|
+
*frame = None;
|
|
817
|
+
}
|
|
818
|
+
stop_signal.send_replace(0);
|
|
819
|
+
let _ = stopped_events.send(Notice {
|
|
820
|
+
session_id: stopped_id.clone(),
|
|
821
|
+
event: if running {
|
|
822
|
+
SessionEvent::State {
|
|
823
|
+
capture: "streaming",
|
|
824
|
+
transport: String::from("connected"),
|
|
825
|
+
first_frame: false,
|
|
826
|
+
}
|
|
827
|
+
} else {
|
|
828
|
+
SessionEvent::CaptureStopped { reason }
|
|
829
|
+
},
|
|
830
|
+
});
|
|
831
|
+
};
|
|
832
|
+
let capture_status = Arc::new(on_capture_status);
|
|
833
|
+
let frame_events = events.clone();
|
|
834
|
+
let frame_session_id = id.clone();
|
|
835
|
+
let sink = Box::new(move |frame: I420, _seq: u64, raw: Vec<u8>| {
|
|
836
|
+
let hashes = tile_hashes(&raw, frame.width, frame.height);
|
|
837
|
+
if let Ok(mut held) = observed.lock() {
|
|
838
|
+
let unchanged =
|
|
839
|
+
held.as_ref()
|
|
840
|
+
.is_some_and(|(_, _, width, height, _, current, _)| {
|
|
841
|
+
*width == frame.width && *height == frame.height && current == &hashes
|
|
842
|
+
});
|
|
843
|
+
if !unchanged {
|
|
844
|
+
let seq = frame_sequence.fetch_add(1, Ordering::Relaxed) + 1;
|
|
845
|
+
let previous = held
|
|
846
|
+
.as_ref()
|
|
847
|
+
.map(|(_, _, _, _, _, current, _)| current.clone())
|
|
848
|
+
.unwrap_or_default();
|
|
849
|
+
let damage = dirty_regions(&hashes, &previous, frame.width, frame.height);
|
|
850
|
+
*held = Some((
|
|
851
|
+
seq,
|
|
852
|
+
Instant::now(),
|
|
853
|
+
frame.width,
|
|
854
|
+
frame.height,
|
|
855
|
+
raw,
|
|
856
|
+
hashes,
|
|
857
|
+
previous,
|
|
858
|
+
));
|
|
859
|
+
frame_tx_signal.send_replace(seq);
|
|
860
|
+
let _ = frame_events.send(Notice {
|
|
861
|
+
session_id: frame_session_id.clone(),
|
|
862
|
+
event: SessionEvent::Frame { seq, damage },
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
}
|
|
566
866
|
if let Ok(mut m) = captured.lock() {
|
|
567
867
|
m.captured_frames += 1;
|
|
568
868
|
}
|
|
@@ -584,15 +884,23 @@ impl Session {
|
|
|
584
884
|
display.as_deref(),
|
|
585
885
|
request.max_width,
|
|
586
886
|
request.max_height,
|
|
587
|
-
|
|
887
|
+
max_fps,
|
|
588
888
|
metrics.clone(),
|
|
589
889
|
sink,
|
|
890
|
+
Box::new({
|
|
891
|
+
let status = capture_status.clone();
|
|
892
|
+
move |reason| status(false, reason)
|
|
893
|
+
}),
|
|
590
894
|
)
|
|
591
895
|
.map_err(|error| SessionError::new("source", format!("{error:#}")))?,
|
|
592
896
|
_ => {
|
|
593
|
-
let portal =
|
|
594
|
-
|
|
595
|
-
|
|
897
|
+
let portal = match request.source.as_ref() {
|
|
898
|
+
Some(SourceRequest::Display { display_id }) => {
|
|
899
|
+
portal::open_display(*display_id).await
|
|
900
|
+
}
|
|
901
|
+
_ => portal::open(request.restore_token.as_deref()).await,
|
|
902
|
+
}
|
|
903
|
+
.map_err(|error| SessionError::new("source", format!("{error:#}")))?;
|
|
596
904
|
if let Some(token) = &portal.restore_token {
|
|
597
905
|
let _ = events.send(Notice {
|
|
598
906
|
session_id: id.clone(),
|
|
@@ -604,8 +912,59 @@ impl Session {
|
|
|
604
912
|
let source_h = source.height.max(1) as usize;
|
|
605
913
|
let (width, height) =
|
|
606
914
|
fit(source_w, source_h, request.max_width, request.max_height);
|
|
607
|
-
|
|
608
|
-
|
|
915
|
+
#[cfg(target_os = "linux")]
|
|
916
|
+
if request.agent_indicator && wants_control {
|
|
917
|
+
indicator = Some(
|
|
918
|
+
crate::indicator::Indicator::start_wayland(source_w, source_h).map_err(
|
|
919
|
+
|error| {
|
|
920
|
+
SessionError::new("indicator-unavailable", format!("{error:#}"))
|
|
921
|
+
},
|
|
922
|
+
)?,
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
#[cfg(target_os = "macos")]
|
|
926
|
+
if request.agent_indicator && wants_control {
|
|
927
|
+
indicator = Some(
|
|
928
|
+
crate::indicator::Indicator::start(source.node_id, source_w, source_h)
|
|
929
|
+
.map_err(|error| {
|
|
930
|
+
SessionError::new("indicator", format!("{error:#}"))
|
|
931
|
+
})?,
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
#[cfg(target_os = "macos")]
|
|
935
|
+
let capture = capture::start(
|
|
936
|
+
portal,
|
|
937
|
+
width,
|
|
938
|
+
height,
|
|
939
|
+
max_fps,
|
|
940
|
+
sink,
|
|
941
|
+
indicator.as_ref().map(crate::indicator::Indicator::pid),
|
|
942
|
+
Box::new({
|
|
943
|
+
let status = capture_status.clone();
|
|
944
|
+
move |running, reason| status(running, reason)
|
|
945
|
+
}),
|
|
946
|
+
)
|
|
947
|
+
.map_err(|error| SessionError::new("source", format!("{error:#}")))?;
|
|
948
|
+
#[cfg(target_os = "linux")]
|
|
949
|
+
let capture = capture::start(
|
|
950
|
+
portal,
|
|
951
|
+
width,
|
|
952
|
+
height,
|
|
953
|
+
max_fps,
|
|
954
|
+
sink,
|
|
955
|
+
indicator
|
|
956
|
+
.as_ref()
|
|
957
|
+
.map(crate::indicator::Indicator::position),
|
|
958
|
+
Box::new({
|
|
959
|
+
let status = capture_status.clone();
|
|
960
|
+
move |reason| status(false, reason)
|
|
961
|
+
}),
|
|
962
|
+
)
|
|
963
|
+
.map_err(|error| SessionError::new("source", format!("{error:#}")))?;
|
|
964
|
+
#[cfg(target_os = "linux")]
|
|
965
|
+
if let Some(indicator) = indicator.as_mut() {
|
|
966
|
+
indicator.event('A', -1, -1);
|
|
967
|
+
}
|
|
609
968
|
Selected {
|
|
610
969
|
source,
|
|
611
970
|
capture: FrameSource::Portal(capture),
|
|
@@ -616,6 +975,20 @@ impl Session {
|
|
|
616
975
|
|
|
617
976
|
let source_w = source.width.max(1) as usize;
|
|
618
977
|
let source_h = source.height.max(1) as usize;
|
|
978
|
+
#[cfg(target_os = "linux")]
|
|
979
|
+
if request.agent_indicator && wants_control && wants_x11 {
|
|
980
|
+
let display = match &request.source {
|
|
981
|
+
Some(SourceRequest::X11 {
|
|
982
|
+
display: Some(display),
|
|
983
|
+
}) => display.clone(),
|
|
984
|
+
_ => std::env::var("DISPLAY")
|
|
985
|
+
.map_err(|_| SessionError::new("indicator-unavailable", "no X display"))?,
|
|
986
|
+
};
|
|
987
|
+
indicator = Some(
|
|
988
|
+
crate::indicator::Indicator::start(&display, source_w, source_h)
|
|
989
|
+
.map_err(|error| SessionError::new("indicator", format!("{error:#}")))?,
|
|
990
|
+
);
|
|
991
|
+
}
|
|
619
992
|
let (width, height) = fit(source_w, source_h, request.max_width, request.max_height);
|
|
620
993
|
|
|
621
994
|
let bitrate_kbps = if request.bitrate_kbps == 0 {
|
|
@@ -627,7 +1000,7 @@ impl Session {
|
|
|
627
1000
|
width,
|
|
628
1001
|
height,
|
|
629
1002
|
bitrate_kbps,
|
|
630
|
-
|
|
1003
|
+
max_fps,
|
|
631
1004
|
available_parallelism().min(8) as u32,
|
|
632
1005
|
)
|
|
633
1006
|
.map_err(|error| SessionError::new("encode", format!("{error:#}")))?;
|
|
@@ -647,11 +1020,19 @@ impl Session {
|
|
|
647
1020
|
wheel_rest: (0.0, 0.0),
|
|
648
1021
|
},
|
|
649
1022
|
None => InputTarget {
|
|
650
|
-
applier: Applier::Uinput(
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
)
|
|
654
|
-
|
|
1023
|
+
applier: Applier::Uinput({
|
|
1024
|
+
#[cfg(target_os = "linux")]
|
|
1025
|
+
let devices = InputDevices::create(source_w as i32, source_h as i32);
|
|
1026
|
+
#[cfg(target_os = "macos")]
|
|
1027
|
+
let devices = InputDevices::create_for_display(
|
|
1028
|
+
source_w as i32,
|
|
1029
|
+
source_h as i32,
|
|
1030
|
+
source.node_id,
|
|
1031
|
+
);
|
|
1032
|
+
devices.map_err(|error| {
|
|
1033
|
+
SessionError::new("input-unavailable", format!("{error:#}"))
|
|
1034
|
+
})?
|
|
1035
|
+
}),
|
|
655
1036
|
held: HeldState::default(),
|
|
656
1037
|
explicit_modifiers: Vec::new(),
|
|
657
1038
|
chord_modifiers: Vec::new(),
|
|
@@ -675,9 +1056,11 @@ impl Session {
|
|
|
675
1056
|
pace_bps: (bitrate_kbps as f64 * 3_000.0).max(20_000_000.0),
|
|
676
1057
|
},
|
|
677
1058
|
peer_events_tx,
|
|
1059
|
+
VideoCodec::Vp9,
|
|
678
1060
|
)
|
|
679
1061
|
.await
|
|
680
1062
|
.map_err(|error| SessionError::new("transport", format!("{error:#}")))?;
|
|
1063
|
+
let peer = Arc::new(peer);
|
|
681
1064
|
|
|
682
1065
|
let generation = 1u64;
|
|
683
1066
|
let geometry = serde_json::json!({
|
|
@@ -694,16 +1077,21 @@ impl Session {
|
|
|
694
1077
|
source,
|
|
695
1078
|
geometry,
|
|
696
1079
|
metrics,
|
|
697
|
-
|
|
698
|
-
|
|
1080
|
+
latest,
|
|
1081
|
+
capture_error,
|
|
1082
|
+
frame_changes,
|
|
1083
|
+
peer: Mutex::new(Some(peer.clone())),
|
|
1084
|
+
encoder: Mutex::new(Some(encoder)),
|
|
699
1085
|
input: Mutex::new(input),
|
|
1086
|
+
indicator: Mutex::new(indicator),
|
|
700
1087
|
capture: Mutex::new(Some(capture)),
|
|
701
|
-
layout: Mutex::new(layout),
|
|
1088
|
+
layout: Mutex::new(Some(layout)),
|
|
702
1089
|
last_seq: Mutex::new(0),
|
|
703
1090
|
control_open: AtomicBool::new(false),
|
|
704
1091
|
closed: AtomicBool::new(false),
|
|
705
1092
|
pipeline: pipeline.clone(),
|
|
706
1093
|
events: events.clone(),
|
|
1094
|
+
encoded: Mutex::new(None),
|
|
707
1095
|
});
|
|
708
1096
|
|
|
709
1097
|
inner.notify(SessionEvent::Description {
|
|
@@ -716,13 +1104,140 @@ impl Session {
|
|
|
716
1104
|
first_frame: false,
|
|
717
1105
|
});
|
|
718
1106
|
|
|
719
|
-
spawn_pipeline(&inner, frame_rx, pipeline,
|
|
1107
|
+
spawn_pipeline(&inner, peer, frame_rx, pipeline, max_fps, bitrate_kbps);
|
|
720
1108
|
spawn_peer_events(&inner, peer_events_rx);
|
|
721
1109
|
spawn_lease(&inner, request.ttl.unwrap_or(Duration::from_secs(3600)));
|
|
722
1110
|
|
|
723
1111
|
Ok(Self { inner })
|
|
724
1112
|
}
|
|
725
1113
|
|
|
1114
|
+
/// Open a session over video the consumer already encoded.
|
|
1115
|
+
///
|
|
1116
|
+
/// Nothing is captured, encoded or driven locally: the engine packetizes
|
|
1117
|
+
/// what it is fed and sends the client's control messages back to the
|
|
1118
|
+
/// consumer, which owns the device. The peer is deliberately absent here —
|
|
1119
|
+
/// the offer must name the profile-level-id of the stream's own SPS, and
|
|
1120
|
+
/// that only exists once the first access unit arrives.
|
|
1121
|
+
async fn open_encoded(
|
|
1122
|
+
request: OpenRequest,
|
|
1123
|
+
codec: EncodedCodec,
|
|
1124
|
+
width: usize,
|
|
1125
|
+
height: usize,
|
|
1126
|
+
events: tokio_mpsc::UnboundedSender<Notice>,
|
|
1127
|
+
) -> std::result::Result<Self, SessionError> {
|
|
1128
|
+
if width == 0 || height == 0 || width > 7680 || height > 4320 {
|
|
1129
|
+
return Err(SessionError::new(
|
|
1130
|
+
"source",
|
|
1131
|
+
format!(
|
|
1132
|
+
"an encoded surface must be 1..7680 by 1..4320 pixels, not {width}x{height}"
|
|
1133
|
+
),
|
|
1134
|
+
));
|
|
1135
|
+
}
|
|
1136
|
+
let id = opaque_id();
|
|
1137
|
+
let generation = 1u64;
|
|
1138
|
+
let metrics = Arc::new(Mutex::new(Metrics::default()));
|
|
1139
|
+
// Nothing raises it, and nothing waits for it: an encoded source has no
|
|
1140
|
+
// frame to read, only an encoded stream to forward.
|
|
1141
|
+
let (_frame_signal, frame_changes) = tokio::sync::watch::channel(0u64);
|
|
1142
|
+
let (peer_events, peer_events_rx) = tokio_mpsc::unbounded_channel::<PeerEvent>();
|
|
1143
|
+
let (feed, feed_rx) = tokio_mpsc::channel::<FedAccessUnit>(FEED_BACKLOG);
|
|
1144
|
+
let geometry = serde_json::json!({
|
|
1145
|
+
"source": { "width": width, "height": height },
|
|
1146
|
+
// Nothing is scaled: the client aims at the stream's own pixels, and
|
|
1147
|
+
// the consumer maps them to its device.
|
|
1148
|
+
"encoded": { "width": width, "height": height },
|
|
1149
|
+
"origin": { "x": 0, "y": 0 },
|
|
1150
|
+
});
|
|
1151
|
+
let inner = Arc::new(Inner {
|
|
1152
|
+
id,
|
|
1153
|
+
generation,
|
|
1154
|
+
permissions: request.permissions,
|
|
1155
|
+
source: SelectedSource {
|
|
1156
|
+
node_id: 0,
|
|
1157
|
+
width: width as i32,
|
|
1158
|
+
height: height as i32,
|
|
1159
|
+
position: Some((0, 0)),
|
|
1160
|
+
source_type: Some(String::from("encoded")),
|
|
1161
|
+
origin_x: 0,
|
|
1162
|
+
origin_y: 0,
|
|
1163
|
+
},
|
|
1164
|
+
geometry,
|
|
1165
|
+
metrics,
|
|
1166
|
+
latest: Arc::new(Mutex::new(None)),
|
|
1167
|
+
capture_error: Arc::new(Mutex::new(None)),
|
|
1168
|
+
frame_changes,
|
|
1169
|
+
peer: Mutex::new(None),
|
|
1170
|
+
encoder: Mutex::new(None),
|
|
1171
|
+
input: Mutex::new(None),
|
|
1172
|
+
indicator: Mutex::new(None),
|
|
1173
|
+
capture: Mutex::new(None),
|
|
1174
|
+
layout: Mutex::new(None),
|
|
1175
|
+
last_seq: Mutex::new(0),
|
|
1176
|
+
control_open: AtomicBool::new(false),
|
|
1177
|
+
closed: AtomicBool::new(false),
|
|
1178
|
+
pipeline: Arc::new(AtomicBool::new(false)),
|
|
1179
|
+
events: events.clone(),
|
|
1180
|
+
encoded: Mutex::new(Some(Encoded {
|
|
1181
|
+
codec,
|
|
1182
|
+
transport: TransportOptions {
|
|
1183
|
+
ice_servers: request.ice_servers,
|
|
1184
|
+
loopback_tcp: request.loopback_tcp,
|
|
1185
|
+
// A device stream is a few megabits; pacing exists to spread
|
|
1186
|
+
// a large frame, not to shape a rate this engine does not set.
|
|
1187
|
+
pace_bps: 20_000_000.0,
|
|
1188
|
+
},
|
|
1189
|
+
peer_events,
|
|
1190
|
+
feed,
|
|
1191
|
+
})),
|
|
1192
|
+
});
|
|
1193
|
+
inner.notify(SessionEvent::State {
|
|
1194
|
+
capture: "consented",
|
|
1195
|
+
transport: String::from("new"),
|
|
1196
|
+
first_frame: false,
|
|
1197
|
+
});
|
|
1198
|
+
spawn_peer_events(&inner, peer_events_rx);
|
|
1199
|
+
spawn_encoded(&inner, feed_rx);
|
|
1200
|
+
spawn_lease(&inner, request.ttl.unwrap_or(Duration::from_secs(3600)));
|
|
1201
|
+
Ok(Self { inner })
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
/// Take one access unit of an encoded source.
|
|
1205
|
+
///
|
|
1206
|
+
/// The reply acknowledges the hand-off, not the picture: the unit is
|
|
1207
|
+
/// packetized behind this request, and `session.metrics` reports what was
|
|
1208
|
+
/// sent and what was dropped.
|
|
1209
|
+
pub fn feed(&self, keyframe: bool, data: Vec<u8>) -> std::result::Result<(), SessionError> {
|
|
1210
|
+
if self.inner.closed.load(Ordering::Relaxed) {
|
|
1211
|
+
return Err(SessionError::new("session", "the session has ended"));
|
|
1212
|
+
}
|
|
1213
|
+
let encoded = lock(&self.inner.encoded);
|
|
1214
|
+
let Some(encoded) = encoded.as_ref() else {
|
|
1215
|
+
return Err(SessionError::new(
|
|
1216
|
+
"operation",
|
|
1217
|
+
"this session has no encoded source to feed",
|
|
1218
|
+
));
|
|
1219
|
+
};
|
|
1220
|
+
if data.is_empty() {
|
|
1221
|
+
return Err(SessionError::new(
|
|
1222
|
+
"operation",
|
|
1223
|
+
"an access unit cannot be empty",
|
|
1224
|
+
));
|
|
1225
|
+
}
|
|
1226
|
+
if let Ok(mut m) = self.inner.metrics.lock() {
|
|
1227
|
+
m.fed_frames += 1;
|
|
1228
|
+
}
|
|
1229
|
+
if encoded
|
|
1230
|
+
.feed
|
|
1231
|
+
.try_send(FedAccessUnit { keyframe, data })
|
|
1232
|
+
.is_err()
|
|
1233
|
+
{
|
|
1234
|
+
if let Ok(mut m) = self.inner.metrics.lock() {
|
|
1235
|
+
m.dropped_frames += 1;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
Ok(())
|
|
1239
|
+
}
|
|
1240
|
+
|
|
726
1241
|
pub fn id(&self) -> &str {
|
|
727
1242
|
&self.inner.id
|
|
728
1243
|
}
|
|
@@ -739,6 +1254,118 @@ impl Session {
|
|
|
739
1254
|
&self.inner.source
|
|
740
1255
|
}
|
|
741
1256
|
|
|
1257
|
+
pub async fn wait_frame(
|
|
1258
|
+
&self,
|
|
1259
|
+
after_seq: Option<u64>,
|
|
1260
|
+
still_ms: Option<u64>,
|
|
1261
|
+
timeout_ms: u64,
|
|
1262
|
+
) -> std::result::Result<(), SessionError> {
|
|
1263
|
+
if lock(&self.inner.encoded).is_some() {
|
|
1264
|
+
return Err(SessionError::new(
|
|
1265
|
+
"operation",
|
|
1266
|
+
"this session forwards an encoded stream and keeps no frame",
|
|
1267
|
+
));
|
|
1268
|
+
}
|
|
1269
|
+
let mut changes = self.inner.frame_changes.clone();
|
|
1270
|
+
let deadline = tokio::time::Instant::now() + Duration::from_millis(timeout_ms.min(120_000));
|
|
1271
|
+
loop {
|
|
1272
|
+
if let Some(reason) = lock(&self.inner.capture_error).clone() {
|
|
1273
|
+
return Err(SessionError::new("stream-stopped", reason));
|
|
1274
|
+
}
|
|
1275
|
+
let (seq, still) = self
|
|
1276
|
+
.inner
|
|
1277
|
+
.latest
|
|
1278
|
+
.lock()
|
|
1279
|
+
.ok()
|
|
1280
|
+
.and_then(|held| held.as_ref().map(|(seq, at, ..)| (*seq, at.elapsed())))
|
|
1281
|
+
.unwrap_or((0, Duration::ZERO));
|
|
1282
|
+
let target = still_ms.or(if after_seq.is_none() { Some(150) } else { None });
|
|
1283
|
+
let advanced = after_seq.is_none_or(|after| seq > after);
|
|
1284
|
+
if seq > 0 && advanced && target.is_none_or(|ms| still >= Duration::from_millis(ms)) {
|
|
1285
|
+
return Ok(());
|
|
1286
|
+
}
|
|
1287
|
+
let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
|
|
1288
|
+
if remaining.is_zero() {
|
|
1289
|
+
return Err(SessionError::new(
|
|
1290
|
+
"frame-timeout",
|
|
1291
|
+
"frame condition not met before deadline",
|
|
1292
|
+
));
|
|
1293
|
+
}
|
|
1294
|
+
let until_still = if seq > 0 && advanced {
|
|
1295
|
+
target.map_or(remaining, |ms| {
|
|
1296
|
+
Duration::from_millis(ms).saturating_sub(still)
|
|
1297
|
+
})
|
|
1298
|
+
} else {
|
|
1299
|
+
remaining
|
|
1300
|
+
};
|
|
1301
|
+
let _ = tokio::time::timeout(remaining.min(until_still), changes.changed()).await;
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
/// Write only the requested lossless frame bytes; JSON carries metadata, never pixels.
|
|
1306
|
+
pub fn frame(
|
|
1307
|
+
&self,
|
|
1308
|
+
since: Option<u64>,
|
|
1309
|
+
path: &str,
|
|
1310
|
+
region: Option<[usize; 4]>,
|
|
1311
|
+
) -> std::result::Result<serde_json::Value, SessionError> {
|
|
1312
|
+
if !self.inner.permissions.contains(&Permission::View) {
|
|
1313
|
+
return Err(SessionError::new(
|
|
1314
|
+
"permission",
|
|
1315
|
+
"view permission is required",
|
|
1316
|
+
));
|
|
1317
|
+
}
|
|
1318
|
+
if lock(&self.inner.encoded).is_some() {
|
|
1319
|
+
return Err(SessionError::new(
|
|
1320
|
+
"operation",
|
|
1321
|
+
"this session forwards an encoded stream and keeps no frame",
|
|
1322
|
+
));
|
|
1323
|
+
}
|
|
1324
|
+
if let Some(reason) = lock(&self.inner.capture_error).clone() {
|
|
1325
|
+
return Err(SessionError::new("stream-stopped", reason));
|
|
1326
|
+
}
|
|
1327
|
+
let held = lock(&self.inner.latest);
|
|
1328
|
+
let (seq, at, width, height, raw, hashes, previous) = held
|
|
1329
|
+
.as_ref()
|
|
1330
|
+
.ok_or_else(|| SessionError::new("frame", "the first frame has not arrived"))?;
|
|
1331
|
+
let [x, y, w, h] = region.unwrap_or([0, 0, *width, *height]);
|
|
1332
|
+
if w == 0
|
|
1333
|
+
|| h == 0
|
|
1334
|
+
|| x.checked_add(w).is_none_or(|end| end > *width)
|
|
1335
|
+
|| y.checked_add(h).is_none_or(|end| end > *height)
|
|
1336
|
+
{
|
|
1337
|
+
return Err(SessionError::new(
|
|
1338
|
+
"coordinates",
|
|
1339
|
+
"region exceeds the encoded surface",
|
|
1340
|
+
));
|
|
1341
|
+
}
|
|
1342
|
+
let damage = if since == Some(*seq) {
|
|
1343
|
+
Vec::new()
|
|
1344
|
+
} else if since == seq.checked_sub(1) && previous.len() == hashes.len() {
|
|
1345
|
+
dirty_regions(hashes, previous, *width, *height)
|
|
1346
|
+
} else {
|
|
1347
|
+
vec![[0, 0, *width, *height]]
|
|
1348
|
+
};
|
|
1349
|
+
// An empty path asks for metadata only; no pixels are copied or written.
|
|
1350
|
+
let pixels = if path.is_empty() {
|
|
1351
|
+
None
|
|
1352
|
+
} else {
|
|
1353
|
+
let mut pixels = Vec::with_capacity(w * h * 4);
|
|
1354
|
+
for row in y..y + h {
|
|
1355
|
+
pixels.extend_from_slice(&raw[(row * width + x) * 4..(row * width + x + w) * 4]);
|
|
1356
|
+
}
|
|
1357
|
+
Some(pixels)
|
|
1358
|
+
};
|
|
1359
|
+
let result = serde_json::json!({ "seq": seq, "still_ms": at.elapsed().as_millis() as u64,
|
|
1360
|
+
"width": w, "height": h, "format": "bgrx", "damage": damage, "written": !path.is_empty() });
|
|
1361
|
+
drop(held);
|
|
1362
|
+
if let Some(pixels) = pixels {
|
|
1363
|
+
std::fs::write(path, pixels)
|
|
1364
|
+
.map_err(|error| SessionError::new("frame", error.to_string()))?;
|
|
1365
|
+
}
|
|
1366
|
+
Ok(result)
|
|
1367
|
+
}
|
|
1368
|
+
|
|
742
1369
|
pub fn metrics(&self) -> Metrics {
|
|
743
1370
|
self.inner
|
|
744
1371
|
.metrics
|
|
@@ -748,7 +1375,32 @@ impl Session {
|
|
|
748
1375
|
}
|
|
749
1376
|
|
|
750
1377
|
pub async fn accept_answer(&self, sdp: String) -> Result<()> {
|
|
751
|
-
self.
|
|
1378
|
+
self.current_peer()?
|
|
1379
|
+
.accept_answer(sdp)
|
|
1380
|
+
.await
|
|
1381
|
+
.map(|_applied| ())
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
/// The peer for this session, which an encoded source does not have until
|
|
1385
|
+
/// its first access unit has named the codec.
|
|
1386
|
+
fn current_peer(&self) -> Result<Arc<VideoPeer>> {
|
|
1387
|
+
self.inner
|
|
1388
|
+
.peer
|
|
1389
|
+
.lock()
|
|
1390
|
+
.ok()
|
|
1391
|
+
.and_then(|peer| peer.clone())
|
|
1392
|
+
.ok_or_else(|| anyhow::anyhow!("no offer has been made for this session yet"))
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
/// Restart ICE: a fresh offer on the same peer, announced as a new
|
|
1396
|
+
/// `session.description` event for the client to answer as usual.
|
|
1397
|
+
pub async fn restart_ice(&self) -> Result<()> {
|
|
1398
|
+
let offer = self.current_peer()?.restart_ice().await?;
|
|
1399
|
+
self.inner.notify(SessionEvent::Description {
|
|
1400
|
+
generation: self.inner.generation,
|
|
1401
|
+
sdp: offer,
|
|
1402
|
+
});
|
|
1403
|
+
Ok(())
|
|
752
1404
|
}
|
|
753
1405
|
|
|
754
1406
|
pub async fn add_candidate(
|
|
@@ -757,8 +1409,7 @@ impl Session {
|
|
|
757
1409
|
sdp_mid: Option<String>,
|
|
758
1410
|
sdp_m_line_index: Option<u16>,
|
|
759
1411
|
) -> Result<()> {
|
|
760
|
-
self.
|
|
761
|
-
.peer
|
|
1412
|
+
self.current_peer()?
|
|
762
1413
|
.add_candidate(candidate, sdp_mid, sdp_m_line_index)
|
|
763
1414
|
.await
|
|
764
1415
|
}
|
|
@@ -819,6 +1470,10 @@ impl Inner {
|
|
|
819
1470
|
}
|
|
820
1471
|
self.control_open.store(false, Ordering::SeqCst);
|
|
821
1472
|
self.pipeline.store(false, Ordering::SeqCst);
|
|
1473
|
+
drop(lock(&self.encoded).take());
|
|
1474
|
+
if let Ok(mut indicator) = self.indicator.lock() {
|
|
1475
|
+
indicator.take();
|
|
1476
|
+
}
|
|
822
1477
|
// Release first, then tear down: a stuck modifier is the one failure the
|
|
823
1478
|
// user cannot undo by reconnecting.
|
|
824
1479
|
if let Ok(mut input) = self.input.lock() {
|
|
@@ -827,14 +1482,16 @@ impl Inner {
|
|
|
827
1482
|
}
|
|
828
1483
|
*input = None;
|
|
829
1484
|
}
|
|
830
|
-
let
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
1485
|
+
if let Some(peer) = self.peer.lock().ok().and_then(|peer| peer.clone()) {
|
|
1486
|
+
let _ = peer
|
|
1487
|
+
.send_control(
|
|
1488
|
+
&serde_json::to_string(&ControlReply::Revoked { reason }).unwrap_or_else(
|
|
1489
|
+
|_| String::from(r#"{"kind":"revoked","reason":"closed"}"#),
|
|
1490
|
+
),
|
|
1491
|
+
)
|
|
1492
|
+
.await;
|
|
1493
|
+
peer.close().await;
|
|
1494
|
+
}
|
|
838
1495
|
// Dropping the capture stops the PipeWire stream and joins its thread,
|
|
839
1496
|
// which is what releases the compositor's consent for this session. It
|
|
840
1497
|
// has to happen synchronously, not when the last handle happens to fall
|
|
@@ -861,7 +1518,10 @@ impl Inner {
|
|
|
861
1518
|
}
|
|
862
1519
|
|
|
863
1520
|
fn clipboard_refusal(&self) -> Option<(&'static str, &'static str)> {
|
|
864
|
-
if
|
|
1521
|
+
if matches!(
|
|
1522
|
+
self.source.source_type.as_deref(),
|
|
1523
|
+
Some("x11-root") | Some("encoded")
|
|
1524
|
+
) || !wayland_clipboard_available()
|
|
865
1525
|
{
|
|
866
1526
|
return Some((
|
|
867
1527
|
"clipboard-unsupported",
|
|
@@ -884,9 +1544,14 @@ impl Inner {
|
|
|
884
1544
|
|
|
885
1545
|
fn reply(&self, payload: &ControlReply<'_>) -> Result<()> {
|
|
886
1546
|
let text = serde_json::to_string(payload)?;
|
|
1547
|
+
let Some(peer) = self.peer.lock().ok().and_then(|peer| peer.clone()) else {
|
|
1548
|
+
// An encoded source has no peer until its first access unit, so a
|
|
1549
|
+
// rejection raised before then has nowhere to go; the request that
|
|
1550
|
+
// caused it is refused in the same breath.
|
|
1551
|
+
return Ok(());
|
|
1552
|
+
};
|
|
887
1553
|
// The channel is asynchronous; a failure here means the peer is gone,
|
|
888
1554
|
// which the connection-state handler closes anyway.
|
|
889
|
-
let peer = self.peer.clone();
|
|
890
1555
|
tokio::spawn(async move {
|
|
891
1556
|
let _ = peer.send_control(&text).await;
|
|
892
1557
|
});
|
|
@@ -922,6 +1587,30 @@ impl Inner {
|
|
|
922
1587
|
}
|
|
923
1588
|
*last = seq;
|
|
924
1589
|
}
|
|
1590
|
+
let feedback = match &message {
|
|
1591
|
+
ControlMessage::Pointer { phase, x, y, .. } if lock(&self.encoded).is_none() => {
|
|
1592
|
+
let (x, y) = to_source_pixels(*x, *y, self.encoded_size(), &self.source);
|
|
1593
|
+
Some((
|
|
1594
|
+
if matches!(phase, PointerPhase::Down) {
|
|
1595
|
+
'C'
|
|
1596
|
+
} else {
|
|
1597
|
+
'M'
|
|
1598
|
+
},
|
|
1599
|
+
x,
|
|
1600
|
+
y,
|
|
1601
|
+
))
|
|
1602
|
+
}
|
|
1603
|
+
ControlMessage::Text { text, .. } if !text.is_empty() => Some(('T', -1, -1)),
|
|
1604
|
+
ControlMessage::Key { down: true, .. } => Some(('T', -1, -1)),
|
|
1605
|
+
_ => None,
|
|
1606
|
+
};
|
|
1607
|
+
// A source the consumer feeds is a device the consumer drives. Nothing is
|
|
1608
|
+
// applied to this machine: the message travels back as an event, and the
|
|
1609
|
+
// consumer injects it where the picture actually came from.
|
|
1610
|
+
if lock(&self.encoded).is_some() {
|
|
1611
|
+
self.forward(message);
|
|
1612
|
+
return;
|
|
1613
|
+
}
|
|
925
1614
|
let outcome = match message {
|
|
926
1615
|
ControlMessage::Pointer {
|
|
927
1616
|
phase,
|
|
@@ -958,6 +1647,13 @@ impl Inner {
|
|
|
958
1647
|
};
|
|
959
1648
|
match outcome {
|
|
960
1649
|
Ok(()) => {
|
|
1650
|
+
if let Some((kind, x, y)) = feedback {
|
|
1651
|
+
if let Ok(mut indicator) = self.indicator.lock() {
|
|
1652
|
+
if let Some(indicator) = indicator.as_mut() {
|
|
1653
|
+
indicator.event(kind, x, y);
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
961
1657
|
if let Ok(mut m) = self.metrics.lock() {
|
|
962
1658
|
m.input_applied += 1;
|
|
963
1659
|
}
|
|
@@ -967,6 +1663,41 @@ impl Inner {
|
|
|
967
1663
|
}
|
|
968
1664
|
}
|
|
969
1665
|
|
|
1666
|
+
/// Hand one client action back to the consumer, which owns the device this
|
|
1667
|
+
/// stream came from. The audit trail is the event itself and the count in
|
|
1668
|
+
/// `session.metrics`; nothing was applied here.
|
|
1669
|
+
fn forward(&self, message: ControlMessage) {
|
|
1670
|
+
let seq = message.seq();
|
|
1671
|
+
// The clipboard belongs to the device the consumer drives, and this
|
|
1672
|
+
// engine has none for this source; saying so is better than a request
|
|
1673
|
+
// that never answers.
|
|
1674
|
+
match &message {
|
|
1675
|
+
ControlMessage::ClipboardRead { request, .. }
|
|
1676
|
+
| ControlMessage::ClipboardWrite { request, .. } => {
|
|
1677
|
+
let _ = self.reply(&ControlReply::Clipboard {
|
|
1678
|
+
request,
|
|
1679
|
+
text: String::new(),
|
|
1680
|
+
truncated: false,
|
|
1681
|
+
error: Some("this source's clipboard belongs to the consumer"),
|
|
1682
|
+
});
|
|
1683
|
+
return;
|
|
1684
|
+
}
|
|
1685
|
+
_ => {}
|
|
1686
|
+
}
|
|
1687
|
+
let input = match serde_json::to_value(&message) {
|
|
1688
|
+
Ok(input) => input,
|
|
1689
|
+
Err(_) => {
|
|
1690
|
+
self.reject(seq, "operation", "unrecognised control message");
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1693
|
+
};
|
|
1694
|
+
self.notify(SessionEvent::Input { input });
|
|
1695
|
+
if let Ok(mut m) = self.metrics.lock() {
|
|
1696
|
+
m.input_forwarded += 1;
|
|
1697
|
+
}
|
|
1698
|
+
let _ = self.reply(&ControlReply::Ack { seq });
|
|
1699
|
+
}
|
|
1700
|
+
|
|
970
1701
|
fn with_input<T>(
|
|
971
1702
|
&self,
|
|
972
1703
|
action: impl FnOnce(&mut InputTarget) -> Result<T>,
|
|
@@ -1043,18 +1774,27 @@ impl Inner {
|
|
|
1043
1774
|
}
|
|
1044
1775
|
|
|
1045
1776
|
let stroke = {
|
|
1046
|
-
let
|
|
1777
|
+
let held = self
|
|
1047
1778
|
.layout
|
|
1048
1779
|
.lock()
|
|
1049
1780
|
.map_err(|_| ("session", String::from("no keyboard layout")))?;
|
|
1781
|
+
let layout = held
|
|
1782
|
+
.as_ref()
|
|
1783
|
+
.ok_or(("session", String::from("no keyboard layout")))?;
|
|
1050
1784
|
if let Some(name) = name {
|
|
1051
1785
|
keymap::named_key(&name)
|
|
1052
1786
|
.ok_or(("text-unsupported", format!("unknown key {name}")))?
|
|
1053
1787
|
} else if let Some(character) = character {
|
|
1054
|
-
let
|
|
1055
|
-
|
|
1788
|
+
let mut characters = character.chars();
|
|
1789
|
+
let wanted = characters
|
|
1056
1790
|
.next()
|
|
1057
1791
|
.ok_or(("text-unsupported", String::from("empty character")))?;
|
|
1792
|
+
if characters.next().is_some() {
|
|
1793
|
+
return Err((
|
|
1794
|
+
"text-unsupported",
|
|
1795
|
+
String::from("character must contain exactly one Unicode scalar"),
|
|
1796
|
+
));
|
|
1797
|
+
}
|
|
1058
1798
|
layout.keystroke_for_char(wanted).ok_or((
|
|
1059
1799
|
"text-unsupported",
|
|
1060
1800
|
format!("the active layout cannot produce {wanted:?}"),
|
|
@@ -1080,42 +1820,63 @@ impl Inner {
|
|
|
1080
1820
|
if text.len() > 4096 {
|
|
1081
1821
|
return Err(("text-too-large", String::from("text exceeds 4096 bytes")));
|
|
1082
1822
|
}
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
.
|
|
1087
|
-
let (plan, unreachable) = layout.plan_text(text);
|
|
1088
|
-
drop(layout);
|
|
1089
|
-
if !unreachable.is_empty() {
|
|
1090
|
-
return Err((
|
|
1091
|
-
"text-unsupported",
|
|
1092
|
-
format!("the active layout cannot produce {unreachable:?}; use the clipboard"),
|
|
1093
|
-
));
|
|
1823
|
+
#[cfg(target_os = "macos")]
|
|
1824
|
+
{
|
|
1825
|
+
let _ = seq;
|
|
1826
|
+
return self.with_input(|target| target.unicode_text(text));
|
|
1094
1827
|
}
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1828
|
+
#[cfg(target_os = "linux")]
|
|
1829
|
+
{
|
|
1830
|
+
let held = self
|
|
1831
|
+
.layout
|
|
1832
|
+
.lock()
|
|
1833
|
+
.map_err(|_| ("session", String::from("no keyboard layout")))?;
|
|
1834
|
+
let layout = held
|
|
1835
|
+
.as_ref()
|
|
1836
|
+
.ok_or(("session", String::from("no keyboard layout")))?;
|
|
1837
|
+
let (plan, unreachable) = layout.plan_text(text);
|
|
1838
|
+
drop(held);
|
|
1839
|
+
if !unreachable.is_empty() {
|
|
1840
|
+
return Err((
|
|
1841
|
+
"text-unsupported",
|
|
1842
|
+
format!("the active layout cannot produce {unreachable:?}; use the clipboard"),
|
|
1843
|
+
));
|
|
1844
|
+
}
|
|
1845
|
+
let _ = seq;
|
|
1846
|
+
self.with_input(|target| {
|
|
1847
|
+
// Uinput's immediate down/up pairs can be collapsed by a compositor's
|
|
1848
|
+
// event loop. XTest flushes each edge, so pace only the virtual device.
|
|
1849
|
+
let paced = matches!(&target.applier, Applier::Uinput(_));
|
|
1850
|
+
let pause = || {
|
|
1851
|
+
if paced {
|
|
1852
|
+
std::thread::sleep(std::time::Duration::from_millis(5));
|
|
1109
1853
|
}
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1854
|
+
};
|
|
1855
|
+
target.check_keys(
|
|
1856
|
+
plan.iter()
|
|
1857
|
+
.flatten()
|
|
1858
|
+
.flat_map(|stroke| std::iter::once(stroke.code).chain(stroke.modifiers())),
|
|
1859
|
+
)?;
|
|
1860
|
+
for keystroke in plan {
|
|
1861
|
+
for stroke in keystroke {
|
|
1862
|
+
let modifiers = stroke.modifiers();
|
|
1863
|
+
for modifier in &modifiers {
|
|
1864
|
+
target.modifier(*modifier, true, false)?;
|
|
1865
|
+
pause();
|
|
1866
|
+
}
|
|
1867
|
+
target.key(stroke.code, true)?;
|
|
1868
|
+
pause();
|
|
1869
|
+
target.key(stroke.code, false)?;
|
|
1870
|
+
pause();
|
|
1871
|
+
for modifier in modifiers.iter().rev() {
|
|
1872
|
+
target.modifier(*modifier, false, false)?;
|
|
1873
|
+
pause();
|
|
1874
|
+
}
|
|
1114
1875
|
}
|
|
1115
1876
|
}
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
}
|
|
1877
|
+
Ok(())
|
|
1878
|
+
})
|
|
1879
|
+
}
|
|
1119
1880
|
}
|
|
1120
1881
|
|
|
1121
1882
|
fn clipboard_read(self: &Arc<Self>, request: String) {
|
|
@@ -1331,6 +2092,7 @@ enum Pass {
|
|
|
1331
2092
|
/// the desktop goes still the last frame is coded once more, sharp.
|
|
1332
2093
|
fn spawn_pipeline(
|
|
1333
2094
|
inner: &Arc<Inner>,
|
|
2095
|
+
peer: Arc<VideoPeer>,
|
|
1334
2096
|
frame_rx: FrameReceiver,
|
|
1335
2097
|
running: Arc<AtomicBool>,
|
|
1336
2098
|
max_fps: u32,
|
|
@@ -1359,7 +2121,7 @@ fn spawn_pipeline(
|
|
|
1359
2121
|
let slot = last_sent.map_or(Duration::ZERO, |at| {
|
|
1360
2122
|
interval.saturating_sub(now.duration_since(at))
|
|
1361
2123
|
});
|
|
1362
|
-
let connected =
|
|
2124
|
+
let connected = peer.is_connected();
|
|
1363
2125
|
let wait = if !connected {
|
|
1364
2126
|
// Nothing sent before the transport connects arrives; the
|
|
1365
2127
|
// first frame after it is a key frame.
|
|
@@ -1395,15 +2157,17 @@ fn spawn_pipeline(
|
|
|
1395
2157
|
if !running.load(Ordering::Relaxed) {
|
|
1396
2158
|
break;
|
|
1397
2159
|
}
|
|
1398
|
-
if
|
|
2160
|
+
if peer.take_keyframe_request() || !connected {
|
|
1399
2161
|
keyframe = true;
|
|
1400
2162
|
}
|
|
1401
|
-
let reports =
|
|
2163
|
+
let reports = peer.take_loss_reports();
|
|
1402
2164
|
if let Some(kbps) = rate.update(&reports, Instant::now()) {
|
|
1403
|
-
let applied = inner
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
2165
|
+
let applied = inner.encoder.lock().map(|mut encoder| {
|
|
2166
|
+
encoder
|
|
2167
|
+
.as_mut()
|
|
2168
|
+
.map(|encoder| encoder.set_bitrate(kbps))
|
|
2169
|
+
.unwrap_or(Ok(()))
|
|
2170
|
+
});
|
|
1407
2171
|
if matches!(applied, Ok(Ok(()))) {
|
|
1408
2172
|
if let Ok(mut m) = inner.metrics.lock() {
|
|
1409
2173
|
m.target_kbps = kbps;
|
|
@@ -1434,10 +2198,13 @@ fn spawn_pipeline(
|
|
|
1434
2198
|
|
|
1435
2199
|
let started = Instant::now();
|
|
1436
2200
|
let packet = {
|
|
1437
|
-
let mut
|
|
1438
|
-
Ok(
|
|
2201
|
+
let mut held = match inner.encoder.lock() {
|
|
2202
|
+
Ok(held) => held,
|
|
1439
2203
|
Err(_) => break,
|
|
1440
2204
|
};
|
|
2205
|
+
let Some(encoder) = held.as_mut() else {
|
|
2206
|
+
break;
|
|
2207
|
+
};
|
|
1441
2208
|
match pass {
|
|
1442
2209
|
Pass::Motion { keyframe } => encoder.encode(frame, keyframe),
|
|
1443
2210
|
Pass::Refine => encoder.refine(frame),
|
|
@@ -1488,7 +2255,6 @@ fn spawn_pipeline(
|
|
|
1488
2255
|
Pass::Keepalive => {}
|
|
1489
2256
|
}
|
|
1490
2257
|
last_sent = Some(now);
|
|
1491
|
-
let peer = inner.peer.clone();
|
|
1492
2258
|
if let Err(error) =
|
|
1493
2259
|
handle.block_on(peer.send_frame(&packet.data, packet.keyframe, now))
|
|
1494
2260
|
{
|
|
@@ -1510,6 +2276,113 @@ fn spawn_pipeline(
|
|
|
1510
2276
|
.ok();
|
|
1511
2277
|
}
|
|
1512
2278
|
|
|
2279
|
+
/// Packetize what a consumer feeds an encoded source, building the peer on the
|
|
2280
|
+
/// first access unit that carries an SPS.
|
|
2281
|
+
///
|
|
2282
|
+
/// That order is forced by the contract: the offer must name the profile the
|
|
2283
|
+
/// stream actually is, and the only place that is written down is the stream's
|
|
2284
|
+
/// own SPS. Until one arrives there is nothing that could be offered.
|
|
2285
|
+
fn spawn_encoded(inner: &Arc<Inner>, mut feed: tokio_mpsc::Receiver<FedAccessUnit>) {
|
|
2286
|
+
let inner = inner.clone();
|
|
2287
|
+
tokio::spawn(async move {
|
|
2288
|
+
while let Some(unit) = feed.recv().await {
|
|
2289
|
+
if inner.closed.load(Ordering::SeqCst) {
|
|
2290
|
+
return;
|
|
2291
|
+
}
|
|
2292
|
+
if current_peer(&inner).is_none() {
|
|
2293
|
+
let Some(profile) = h264_profile_level_id(&unit.data) else {
|
|
2294
|
+
if let Ok(mut m) = inner.metrics.lock() {
|
|
2295
|
+
m.dropped_frames += 1;
|
|
2296
|
+
}
|
|
2297
|
+
continue;
|
|
2298
|
+
};
|
|
2299
|
+
let (transport, peer_events, codec) = {
|
|
2300
|
+
let encoded = lock(&inner.encoded);
|
|
2301
|
+
let Some(encoded) = encoded.as_ref() else {
|
|
2302
|
+
return;
|
|
2303
|
+
};
|
|
2304
|
+
let codec = match encoded.codec {
|
|
2305
|
+
EncodedCodec::H264 => VideoCodec::H264 {
|
|
2306
|
+
profile_level_id: profile,
|
|
2307
|
+
},
|
|
2308
|
+
};
|
|
2309
|
+
(
|
|
2310
|
+
encoded.transport.clone(),
|
|
2311
|
+
encoded.peer_events.clone(),
|
|
2312
|
+
codec,
|
|
2313
|
+
)
|
|
2314
|
+
};
|
|
2315
|
+
let offered = VideoPeer::offer(transport, peer_events, codec).await;
|
|
2316
|
+
match offered {
|
|
2317
|
+
Ok((peer, offer)) => {
|
|
2318
|
+
let peer = Arc::new(peer);
|
|
2319
|
+
let stored = {
|
|
2320
|
+
let mut held = lock(&inner.peer);
|
|
2321
|
+
if inner.closed.load(Ordering::SeqCst) {
|
|
2322
|
+
false
|
|
2323
|
+
} else {
|
|
2324
|
+
*held = Some(peer.clone());
|
|
2325
|
+
true
|
|
2326
|
+
}
|
|
2327
|
+
};
|
|
2328
|
+
if !stored {
|
|
2329
|
+
peer.close().await;
|
|
2330
|
+
return;
|
|
2331
|
+
}
|
|
2332
|
+
inner.notify(SessionEvent::Description {
|
|
2333
|
+
generation: inner.generation,
|
|
2334
|
+
sdp: offer,
|
|
2335
|
+
});
|
|
2336
|
+
}
|
|
2337
|
+
Err(error) => {
|
|
2338
|
+
revoke(
|
|
2339
|
+
&inner,
|
|
2340
|
+
format!("the transport refused the encoded stream: {error:#}"),
|
|
2341
|
+
)
|
|
2342
|
+
.await;
|
|
2343
|
+
return;
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
let Some(peer) = current_peer(&inner) else {
|
|
2348
|
+
return;
|
|
2349
|
+
};
|
|
2350
|
+
let started = Instant::now();
|
|
2351
|
+
let sent = peer.send_frame(&unit.data, unit.keyframe, started).await;
|
|
2352
|
+
match sent {
|
|
2353
|
+
Ok(()) => {
|
|
2354
|
+
if let Ok(mut m) = inner.metrics.lock() {
|
|
2355
|
+
m.encoded_frames += 1;
|
|
2356
|
+
m.encoded_bytes += unit.data.len() as u64;
|
|
2357
|
+
m.encode_micros += started.elapsed().as_micros() as u64;
|
|
2358
|
+
if unit.keyframe {
|
|
2359
|
+
m.key_frames += 1;
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
Err(error) => {
|
|
2364
|
+
eprintln!("the video track refused a fed access unit: {error:#}");
|
|
2365
|
+
revoke(&inner, String::from("the connection to the phone was lost")).await;
|
|
2366
|
+
return;
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
});
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
fn current_peer(inner: &Arc<Inner>) -> Option<Arc<VideoPeer>> {
|
|
2374
|
+
inner.peer.lock().ok().and_then(|peer| peer.clone())
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
/// End the session with a reason the consumer can show, exactly as the encode
|
|
2378
|
+
/// pipeline does when its transport goes away.
|
|
2379
|
+
async fn revoke(inner: &Arc<Inner>, reason: String) {
|
|
2380
|
+
inner.notify(SessionEvent::Revoked {
|
|
2381
|
+
reason: reason.clone(),
|
|
2382
|
+
});
|
|
2383
|
+
inner.close(&reason).await;
|
|
2384
|
+
}
|
|
2385
|
+
|
|
1513
2386
|
fn spawn_peer_events(inner: &Arc<Inner>, mut events: tokio_mpsc::UnboundedReceiver<PeerEvent>) {
|
|
1514
2387
|
let inner = inner.clone();
|
|
1515
2388
|
tokio::spawn(async move {
|
|
@@ -1532,6 +2405,16 @@ fn spawn_peer_events(inner: &Arc<Inner>, mut events: tokio_mpsc::UnboundedReceiv
|
|
|
1532
2405
|
}
|
|
1533
2406
|
PeerEvent::State(state) => {
|
|
1534
2407
|
use webrtc::peer_connection::RTCPeerConnectionState as State;
|
|
2408
|
+
// The far end needs a reference frame to start from, and only
|
|
2409
|
+
// the consumer can make one for a stream it encodes. A
|
|
2410
|
+
// captured desktop manages this in its encode loop; a fed one
|
|
2411
|
+
// has to ask — and everything sent before the transport
|
|
2412
|
+
// connected was dropped, so this is not optional.
|
|
2413
|
+
if lock(&inner.encoded).is_some() && matches!(state, State::Connected) {
|
|
2414
|
+
inner.notify(SessionEvent::KeyframeRequest {
|
|
2415
|
+
generation: inner.generation,
|
|
2416
|
+
});
|
|
2417
|
+
}
|
|
1535
2418
|
// ICE loss need not close SCTP. Never leave a drag or chord
|
|
1536
2419
|
// held while waiting for that independent notification.
|
|
1537
2420
|
if matches!(state, State::Disconnected | State::Failed | State::Closed) {
|
|
@@ -1578,6 +2461,11 @@ fn spawn_peer_events(inner: &Arc<Inner>, mut events: tokio_mpsc::UnboundedReceiv
|
|
|
1578
2461
|
}
|
|
1579
2462
|
}
|
|
1580
2463
|
}
|
|
2464
|
+
PeerEvent::KeyframeRequest => {
|
|
2465
|
+
inner.notify(SessionEvent::KeyframeRequest {
|
|
2466
|
+
generation: inner.generation,
|
|
2467
|
+
});
|
|
2468
|
+
}
|
|
1581
2469
|
PeerEvent::ControlMessage(text) => {
|
|
1582
2470
|
match serde_json::from_str::<ControlMessage>(&text) {
|
|
1583
2471
|
Ok(message) => inner.apply(message),
|
|
@@ -1639,6 +2527,79 @@ fn opaque_id() -> String {
|
|
|
1639
2527
|
mod tests {
|
|
1640
2528
|
use super::*;
|
|
1641
2529
|
|
|
2530
|
+
#[tokio::test]
|
|
2531
|
+
async fn a_closed_encoded_session_ends_the_tasks_its_channels_park() {
|
|
2532
|
+
let (events, _received) = tokio_mpsc::unbounded_channel();
|
|
2533
|
+
let (peer_events, peer_events_rx) = tokio_mpsc::unbounded_channel::<PeerEvent>();
|
|
2534
|
+
let (feed, feed_rx) = tokio_mpsc::channel::<FedAccessUnit>(FEED_BACKLOG);
|
|
2535
|
+
let inner = Arc::new(Inner {
|
|
2536
|
+
id: String::from("encoded-session"),
|
|
2537
|
+
generation: 1,
|
|
2538
|
+
permissions: Vec::new(),
|
|
2539
|
+
source: SelectedSource {
|
|
2540
|
+
node_id: 0,
|
|
2541
|
+
width: 640,
|
|
2542
|
+
height: 480,
|
|
2543
|
+
position: None,
|
|
2544
|
+
source_type: Some(String::from("encoded")),
|
|
2545
|
+
origin_x: 0,
|
|
2546
|
+
origin_y: 0,
|
|
2547
|
+
},
|
|
2548
|
+
geometry: serde_json::json!({
|
|
2549
|
+
"source": { "width": 640, "height": 480 },
|
|
2550
|
+
"encoded": { "width": 640, "height": 480 },
|
|
2551
|
+
"origin": { "x": 0, "y": 0 },
|
|
2552
|
+
}),
|
|
2553
|
+
metrics: Arc::new(Mutex::new(Metrics::default())),
|
|
2554
|
+
latest: Arc::new(Mutex::new(None)),
|
|
2555
|
+
capture_error: Arc::new(Mutex::new(None)),
|
|
2556
|
+
frame_changes: tokio::sync::watch::channel(0).1,
|
|
2557
|
+
peer: Mutex::new(None),
|
|
2558
|
+
encoder: Mutex::new(None),
|
|
2559
|
+
input: Mutex::new(None),
|
|
2560
|
+
indicator: Mutex::new(None),
|
|
2561
|
+
capture: Mutex::new(None),
|
|
2562
|
+
layout: Mutex::new(None),
|
|
2563
|
+
last_seq: Mutex::new(0),
|
|
2564
|
+
control_open: AtomicBool::new(false),
|
|
2565
|
+
closed: AtomicBool::new(false),
|
|
2566
|
+
pipeline: Arc::new(AtomicBool::new(false)),
|
|
2567
|
+
events,
|
|
2568
|
+
encoded: Mutex::new(Some(Encoded {
|
|
2569
|
+
codec: EncodedCodec::H264,
|
|
2570
|
+
transport: TransportOptions {
|
|
2571
|
+
ice_servers: Vec::new(),
|
|
2572
|
+
loopback_tcp: false,
|
|
2573
|
+
pace_bps: 20_000_000.0,
|
|
2574
|
+
},
|
|
2575
|
+
peer_events,
|
|
2576
|
+
feed,
|
|
2577
|
+
})),
|
|
2578
|
+
});
|
|
2579
|
+
spawn_peer_events(&inner, peer_events_rx);
|
|
2580
|
+
spawn_encoded(&inner, feed_rx);
|
|
2581
|
+
inner.close("test").await;
|
|
2582
|
+
for _ in 0..100 {
|
|
2583
|
+
if Arc::strong_count(&inner) == 1 {
|
|
2584
|
+
break;
|
|
2585
|
+
}
|
|
2586
|
+
tokio::time::sleep(Duration::from_millis(5)).await;
|
|
2587
|
+
}
|
|
2588
|
+
assert_eq!(
|
|
2589
|
+
Arc::strong_count(&inner),
|
|
2590
|
+
1,
|
|
2591
|
+
"closing must end the tasks whose channels the close takes away",
|
|
2592
|
+
);
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
#[test]
|
|
2596
|
+
fn tile_damage_at_origin_does_not_underflow_or_hide_other_tiles() {
|
|
2597
|
+
assert_eq!(
|
|
2598
|
+
dirty_regions(&[1, 0, 0, 1], &[0; 4], 64, 64),
|
|
2599
|
+
vec![[0, 0, 32, 32], [32, 32, 32, 32]]
|
|
2600
|
+
);
|
|
2601
|
+
}
|
|
2602
|
+
|
|
1642
2603
|
#[test]
|
|
1643
2604
|
fn a_small_source_is_never_upscaled() {
|
|
1644
2605
|
assert_eq!(fit(1000, 600, 1280, 800), (1000, 600));
|
|
@@ -1693,6 +2654,7 @@ mod tests {
|
|
|
1693
2654
|
pace_bps: 20_000_000.0,
|
|
1694
2655
|
},
|
|
1695
2656
|
peer_events,
|
|
2657
|
+
VideoCodec::Vp9,
|
|
1696
2658
|
)
|
|
1697
2659
|
.await
|
|
1698
2660
|
.expect("a peer connection builds without a network");
|
|
@@ -1718,8 +2680,11 @@ mod tests {
|
|
|
1718
2680
|
"origin": { "x": 0, "y": 0 },
|
|
1719
2681
|
}),
|
|
1720
2682
|
metrics: Arc::new(Mutex::new(Metrics::default())),
|
|
1721
|
-
|
|
1722
|
-
|
|
2683
|
+
latest: Arc::new(Mutex::new(None)),
|
|
2684
|
+
capture_error: Arc::new(Mutex::new(None)),
|
|
2685
|
+
frame_changes: tokio::sync::watch::channel(0).1,
|
|
2686
|
+
peer: Mutex::new(Some(Arc::new(peer))),
|
|
2687
|
+
encoder: Mutex::new(Some(Encoder::new(64, 64, 1000, 30, 1).expect("an encoder"))),
|
|
1723
2688
|
input: Mutex::new(Some(InputTarget {
|
|
1724
2689
|
applier: Applier::Recording(recorded.clone()),
|
|
1725
2690
|
held: HeldState::default(),
|
|
@@ -1729,12 +2694,14 @@ mod tests {
|
|
|
1729
2694
|
wheel_rest: (0.0, 0.0),
|
|
1730
2695
|
})),
|
|
1731
2696
|
capture: Mutex::new(None),
|
|
1732
|
-
|
|
2697
|
+
indicator: Mutex::new(None),
|
|
2698
|
+
layout: Mutex::new(Some(Layout::from_environment().expect("a keymap"))),
|
|
1733
2699
|
last_seq: Mutex::new(0),
|
|
1734
2700
|
control_open: AtomicBool::new(true),
|
|
1735
2701
|
closed: AtomicBool::new(false),
|
|
1736
2702
|
pipeline: Arc::new(AtomicBool::new(true)),
|
|
1737
2703
|
events,
|
|
2704
|
+
encoded: Mutex::new(None),
|
|
1738
2705
|
});
|
|
1739
2706
|
(inner, recorded)
|
|
1740
2707
|
}
|
|
@@ -2015,12 +2982,41 @@ mod tests {
|
|
|
2015
2982
|
assert_eq!(inner.metrics.lock().unwrap().input_rejected, 2);
|
|
2016
2983
|
}
|
|
2017
2984
|
|
|
2985
|
+
#[tokio::test]
|
|
2986
|
+
async fn a_stopped_capture_never_serves_the_last_frame() {
|
|
2987
|
+
let (events, _) = tokio_mpsc::unbounded_channel();
|
|
2988
|
+
let (mut inner, _) = test_inner(events).await;
|
|
2989
|
+
Arc::get_mut(&mut inner)
|
|
2990
|
+
.unwrap()
|
|
2991
|
+
.permissions
|
|
2992
|
+
.push(Permission::View);
|
|
2993
|
+
let session = Session { inner };
|
|
2994
|
+
*lock(&session.inner.latest) =
|
|
2995
|
+
Some((1, Instant::now(), 2, 2, vec![0; 16], vec![1], vec![]));
|
|
2996
|
+
*lock(&session.inner.capture_error) = Some(String::from("SCStreamErrorDomain -3821"));
|
|
2997
|
+
assert_eq!(
|
|
2998
|
+
session.frame(None, "", None).unwrap_err().code,
|
|
2999
|
+
"stream-stopped"
|
|
3000
|
+
);
|
|
3001
|
+
assert_eq!(
|
|
3002
|
+
session.wait_frame(None, None, 10).await.unwrap_err().code,
|
|
3003
|
+
"stream-stopped"
|
|
3004
|
+
);
|
|
3005
|
+
}
|
|
3006
|
+
|
|
2018
3007
|
#[tokio::test]
|
|
2019
3008
|
async fn a_frame_the_encoder_refuses_ends_the_session_with_a_reason() {
|
|
2020
3009
|
let (events, mut received) = tokio_mpsc::unbounded_channel();
|
|
2021
3010
|
let (inner, _recorded) = test_inner(events).await;
|
|
2022
3011
|
let (frame_tx, frame_rx) = latest_frame();
|
|
2023
|
-
spawn_pipeline(
|
|
3012
|
+
spawn_pipeline(
|
|
3013
|
+
&inner,
|
|
3014
|
+
current_peer(&inner).expect("the test's peer"),
|
|
3015
|
+
frame_rx,
|
|
3016
|
+
Arc::new(AtomicBool::new(true)),
|
|
3017
|
+
30,
|
|
3018
|
+
1000,
|
|
3019
|
+
);
|
|
2024
3020
|
|
|
2025
3021
|
// The encoder is 64x64; a 32x32 frame is the dimension mismatch that
|
|
2026
3022
|
// used to be counted and dropped behind a permanently black picture.
|
|
@@ -2052,6 +3048,210 @@ mod tests {
|
|
|
2052
3048
|
);
|
|
2053
3049
|
}
|
|
2054
3050
|
|
|
3051
|
+
/// A consumer-fed stream: a keyframe carrying SPS, PPS and an IDR slice.
|
|
3052
|
+
fn fed_keyframe() -> Vec<u8> {
|
|
3053
|
+
let mut stream = vec![0x00, 0x00, 0x00, 0x01];
|
|
3054
|
+
stream.extend_from_slice(&[0x67, 0x42, 0xc0, 0x29, 0x8c, 0x8d]);
|
|
3055
|
+
stream.extend_from_slice(&[0x00, 0x00, 0x01, 0x68, 0xce, 0x3c, 0x80]);
|
|
3056
|
+
stream.extend_from_slice(&[0x00, 0x00, 0x01, 0x65, 0x88, 0x84, 0x00]);
|
|
3057
|
+
stream
|
|
3058
|
+
}
|
|
3059
|
+
|
|
3060
|
+
fn encoded_open() -> OpenRequest {
|
|
3061
|
+
OpenRequest {
|
|
3062
|
+
source: Some(SourceRequest::Encoded {
|
|
3063
|
+
codec: EncodedCodec::H264,
|
|
3064
|
+
width: 486,
|
|
3065
|
+
height: 1080,
|
|
3066
|
+
}),
|
|
3067
|
+
permissions: vec![Permission::View, Permission::Control],
|
|
3068
|
+
max_width: 3840,
|
|
3069
|
+
max_height: 2160,
|
|
3070
|
+
bitrate_kbps: 0,
|
|
3071
|
+
max_fps: 30,
|
|
3072
|
+
ice_servers: Vec::new(),
|
|
3073
|
+
restore_token: None,
|
|
3074
|
+
ttl: Some(Duration::from_secs(30)),
|
|
3075
|
+
loopback_tcp: false,
|
|
3076
|
+
agent_indicator: false,
|
|
3077
|
+
}
|
|
3078
|
+
}
|
|
3079
|
+
|
|
3080
|
+
async fn next_notice(received: &mut tokio_mpsc::UnboundedReceiver<Notice>) -> Notice {
|
|
3081
|
+
tokio::time::timeout(Duration::from_secs(5), received.recv())
|
|
3082
|
+
.await
|
|
3083
|
+
.expect("a notification arrives")
|
|
3084
|
+
.expect("the channel stays open")
|
|
3085
|
+
}
|
|
3086
|
+
|
|
3087
|
+
#[tokio::test]
|
|
3088
|
+
async fn an_encoded_session_opens_without_capture_encoder_or_input() {
|
|
3089
|
+
let (events, _received) = tokio_mpsc::unbounded_channel();
|
|
3090
|
+
let session = Session::open(encoded_open(), events)
|
|
3091
|
+
.await
|
|
3092
|
+
.expect("an encoded source needs no display and no input device");
|
|
3093
|
+
let inner = &session.inner;
|
|
3094
|
+
assert_eq!(session.restore_source(), "encoded");
|
|
3095
|
+
assert_eq!(session.geometry()["encoded"]["width"], 486);
|
|
3096
|
+
assert_eq!(session.geometry()["encoded"]["height"], 1080);
|
|
3097
|
+
assert!(
|
|
3098
|
+
inner.encoder.lock().unwrap().is_none(),
|
|
3099
|
+
"the consumer is the encoder for this source"
|
|
3100
|
+
);
|
|
3101
|
+
assert!(
|
|
3102
|
+
inner.capture.lock().unwrap().is_none(),
|
|
3103
|
+
"there is nothing to capture"
|
|
3104
|
+
);
|
|
3105
|
+
assert!(
|
|
3106
|
+
inner.input.lock().unwrap().is_none(),
|
|
3107
|
+
"the client's input belongs to the consumer's device"
|
|
3108
|
+
);
|
|
3109
|
+
assert!(
|
|
3110
|
+
inner.peer.lock().unwrap().is_none(),
|
|
3111
|
+
"the offer waits for the stream's own SPS"
|
|
3112
|
+
);
|
|
3113
|
+
}
|
|
3114
|
+
|
|
3115
|
+
#[tokio::test]
|
|
3116
|
+
async fn the_offer_for_a_fed_stream_names_that_streams_own_profile() {
|
|
3117
|
+
let (events, mut received) = tokio_mpsc::unbounded_channel();
|
|
3118
|
+
let session = Session::open(encoded_open(), events)
|
|
3119
|
+
.await
|
|
3120
|
+
.expect("an encoded session");
|
|
3121
|
+
let first = next_notice(&mut received).await;
|
|
3122
|
+
assert!(
|
|
3123
|
+
matches!(
|
|
3124
|
+
first.event,
|
|
3125
|
+
SessionEvent::State {
|
|
3126
|
+
capture: "consented",
|
|
3127
|
+
..
|
|
3128
|
+
}
|
|
3129
|
+
),
|
|
3130
|
+
"the session is open before any offer exists: {:?}",
|
|
3131
|
+
first.event,
|
|
3132
|
+
);
|
|
3133
|
+
|
|
3134
|
+
session
|
|
3135
|
+
.feed(true, fed_keyframe())
|
|
3136
|
+
.expect("a keyframe is taken");
|
|
3137
|
+
let offered = loop {
|
|
3138
|
+
let notice = next_notice(&mut received).await;
|
|
3139
|
+
if let SessionEvent::Description { sdp, .. } = notice.event {
|
|
3140
|
+
break sdp;
|
|
3141
|
+
}
|
|
3142
|
+
};
|
|
3143
|
+
assert!(
|
|
3144
|
+
offered.contains("H264/90000"),
|
|
3145
|
+
"the fed track is offered as H.264: {offered}"
|
|
3146
|
+
);
|
|
3147
|
+
assert!(
|
|
3148
|
+
offered.contains("profile-level-id=42c029"),
|
|
3149
|
+
"the profile-level-id comes from the fed SPS, not a default: {offered}"
|
|
3150
|
+
);
|
|
3151
|
+
assert_eq!(session.metrics().fed_frames, 1);
|
|
3152
|
+
}
|
|
3153
|
+
|
|
3154
|
+
#[tokio::test]
|
|
3155
|
+
async fn a_fed_stream_forwards_its_controls_instead_of_applying_them() {
|
|
3156
|
+
let (events, mut received) = tokio_mpsc::unbounded_channel();
|
|
3157
|
+
let session = Session::open(encoded_open(), events)
|
|
3158
|
+
.await
|
|
3159
|
+
.expect("an encoded session");
|
|
3160
|
+
session.inner.control_open.store(true, Ordering::SeqCst);
|
|
3161
|
+
|
|
3162
|
+
session.inner.apply(ControlMessage::Pointer {
|
|
3163
|
+
phase: PointerPhase::Down,
|
|
3164
|
+
x: 10,
|
|
3165
|
+
y: 20,
|
|
3166
|
+
button: 1,
|
|
3167
|
+
seq: 1,
|
|
3168
|
+
});
|
|
3169
|
+
|
|
3170
|
+
let forwarded = loop {
|
|
3171
|
+
let notice = next_notice(&mut received).await;
|
|
3172
|
+
if let SessionEvent::Input { input } = notice.event {
|
|
3173
|
+
break input;
|
|
3174
|
+
}
|
|
3175
|
+
};
|
|
3176
|
+
assert_eq!(forwarded["kind"], "pointer");
|
|
3177
|
+
assert_eq!(forwarded["phase"], "down");
|
|
3178
|
+
assert_eq!(forwarded["x"], 10);
|
|
3179
|
+
assert_eq!(forwarded["seq"], 1);
|
|
3180
|
+
assert_eq!(session.metrics().input_forwarded, 1);
|
|
3181
|
+
assert_eq!(
|
|
3182
|
+
session.metrics().input_applied,
|
|
3183
|
+
0,
|
|
3184
|
+
"nothing was applied to this machine"
|
|
3185
|
+
);
|
|
3186
|
+
|
|
3187
|
+
// The stream's own coordinates are what travels, so an encoded surface
|
|
3188
|
+
// cannot be driven by a coordinate the far end never rendered.
|
|
3189
|
+
session.inner.apply(ControlMessage::Pointer {
|
|
3190
|
+
phase: PointerPhase::Move,
|
|
3191
|
+
x: 9_000,
|
|
3192
|
+
y: 20,
|
|
3193
|
+
button: 1,
|
|
3194
|
+
seq: 2,
|
|
3195
|
+
});
|
|
3196
|
+
assert_eq!(session.metrics().input_forwarded, 2);
|
|
3197
|
+
}
|
|
3198
|
+
|
|
3199
|
+
#[tokio::test]
|
|
3200
|
+
async fn a_capture_session_refuses_a_fed_stream() {
|
|
3201
|
+
let (events, _received) = tokio_mpsc::unbounded_channel();
|
|
3202
|
+
let (inner, _recorded) = test_inner(events).await;
|
|
3203
|
+
let session = Session { inner };
|
|
3204
|
+
assert_eq!(
|
|
3205
|
+
session.feed(true, fed_keyframe()).unwrap_err().code,
|
|
3206
|
+
"operation",
|
|
3207
|
+
"only an encoded source takes access units",
|
|
3208
|
+
);
|
|
3209
|
+
}
|
|
3210
|
+
|
|
3211
|
+
#[tokio::test]
|
|
3212
|
+
async fn an_encoded_session_has_no_frame_to_read() {
|
|
3213
|
+
let (events, _received) = tokio_mpsc::unbounded_channel();
|
|
3214
|
+
let session = Session::open(encoded_open(), events)
|
|
3215
|
+
.await
|
|
3216
|
+
.expect("an encoded session");
|
|
3217
|
+
assert_eq!(session.frame(None, "", None).unwrap_err().code, "operation");
|
|
3218
|
+
assert_eq!(
|
|
3219
|
+
session.wait_frame(None, None, 10).await.unwrap_err().code,
|
|
3220
|
+
"operation"
|
|
3221
|
+
);
|
|
3222
|
+
assert_eq!(
|
|
3223
|
+
session.read_clipboard().await.unwrap_err(),
|
|
3224
|
+
"clipboard is unavailable for this desktop source",
|
|
3225
|
+
);
|
|
3226
|
+
}
|
|
3227
|
+
|
|
3228
|
+
#[tokio::test]
|
|
3229
|
+
async fn opening_without_view_permission_is_refused_before_capture() {
|
|
3230
|
+
let (events, _received) = tokio_mpsc::unbounded_channel();
|
|
3231
|
+
let result = Session::open(
|
|
3232
|
+
OpenRequest {
|
|
3233
|
+
source: None,
|
|
3234
|
+
permissions: vec![Permission::Control],
|
|
3235
|
+
max_width: 640,
|
|
3236
|
+
max_height: 480,
|
|
3237
|
+
bitrate_kbps: 0,
|
|
3238
|
+
max_fps: 30,
|
|
3239
|
+
ice_servers: Vec::new(),
|
|
3240
|
+
restore_token: None,
|
|
3241
|
+
ttl: None,
|
|
3242
|
+
loopback_tcp: false,
|
|
3243
|
+
agent_indicator: false,
|
|
3244
|
+
},
|
|
3245
|
+
events,
|
|
3246
|
+
)
|
|
3247
|
+
.await;
|
|
3248
|
+
let error = match result {
|
|
3249
|
+
Ok(_) => panic!("a viewless session must not open"),
|
|
3250
|
+
Err(error) => error,
|
|
3251
|
+
};
|
|
3252
|
+
assert_eq!(error.code, "permission");
|
|
3253
|
+
}
|
|
3254
|
+
|
|
2055
3255
|
#[tokio::test]
|
|
2056
3256
|
async fn a_lost_transport_releases_input_then_ends_the_session_without_sctp_close() {
|
|
2057
3257
|
use webrtc::peer_connection::RTCPeerConnectionState as State;
|
|
@@ -2108,7 +3308,14 @@ mod tests {
|
|
|
2108
3308
|
let (events, _events_rx) = tokio_mpsc::unbounded_channel();
|
|
2109
3309
|
let (inner, _recorded) = test_inner(events).await;
|
|
2110
3310
|
let (frame_tx, frame_rx) = latest_frame();
|
|
2111
|
-
spawn_pipeline(
|
|
3311
|
+
spawn_pipeline(
|
|
3312
|
+
&inner,
|
|
3313
|
+
current_peer(&inner).expect("the test's peer"),
|
|
3314
|
+
frame_rx,
|
|
3315
|
+
Arc::new(AtomicBool::new(true)),
|
|
3316
|
+
20,
|
|
3317
|
+
1000,
|
|
3318
|
+
);
|
|
2112
3319
|
|
|
2113
3320
|
// 40 frames over 200 ms is 200 fps; the requested 20 fps caps what leaves.
|
|
2114
3321
|
for _ in 0..40 {
|