@desklink/host 0.1.1 → 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/README.md +29 -7
- package/bin/desklink-host.mjs +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/resolveEngine.d.ts +2 -2
- package/dist/resolveEngine.d.ts.map +1 -1
- package/dist/resolveEngine.js +4 -4
- package/dist/resolveEngine.js.map +1 -1
- package/docs/BOUNDARY.md +7 -3
- package/docs/PROTOCOL.md +205 -9
- package/engine/Cargo.lock +2 -1
- package/engine/Cargo.toml +2 -1
- package/engine/src/keeper.rs +622 -0
- package/engine/src/mac.rs +3 -3
- package/engine/src/main.rs +42 -0
- package/engine/src/peer.rs +429 -22
- package/engine/src/protocol.rs +138 -2
- package/engine/src/session.rs +768 -55
- 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,6 +61,11 @@ 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
71
|
fn tile_hashes(raw: &[u8], width: usize, height: usize) -> Vec<u64> {
|
|
@@ -460,7 +467,7 @@ fn x11_button(button: Button) -> u8 {
|
|
|
460
467
|
}
|
|
461
468
|
}
|
|
462
469
|
|
|
463
|
-
fn lock<T>(mutex: &
|
|
470
|
+
fn lock<T>(mutex: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
|
|
464
471
|
// A poisoned lock means a previous input call panicked while applying; the
|
|
465
472
|
// desktop is then in unknown state, so the session is closed rather than
|
|
466
473
|
// continuing to drive it.
|
|
@@ -470,6 +477,7 @@ fn lock<T>(mutex: &Arc<Mutex<T>>) -> std::sync::MutexGuard<'_, T> {
|
|
|
470
477
|
}
|
|
471
478
|
|
|
472
479
|
/// Why a session could not do what was asked, in the protocol's own vocabulary.
|
|
480
|
+
#[derive(Debug)]
|
|
473
481
|
pub struct SessionError {
|
|
474
482
|
pub code: &'static str,
|
|
475
483
|
pub message: String,
|
|
@@ -522,8 +530,22 @@ pub enum SessionEvent {
|
|
|
522
530
|
seq: u64,
|
|
523
531
|
damage: Vec<[usize; 4]>,
|
|
524
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
|
+
},
|
|
525
545
|
RestoreToken(String),
|
|
526
|
-
CaptureStopped {
|
|
546
|
+
CaptureStopped {
|
|
547
|
+
reason: String,
|
|
548
|
+
},
|
|
527
549
|
Revoked {
|
|
528
550
|
reason: String,
|
|
529
551
|
},
|
|
@@ -552,12 +574,18 @@ struct Inner {
|
|
|
552
574
|
latest: Arc<Mutex<Option<FrameSnapshot>>>,
|
|
553
575
|
capture_error: Arc<Mutex<Option<String>>>,
|
|
554
576
|
frame_changes: tokio::sync::watch::Receiver<u64>,
|
|
555
|
-
|
|
556
|
-
|
|
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>>,
|
|
557
583
|
input: Mutex<Option<InputTarget>>,
|
|
558
584
|
indicator: Mutex<Option<crate::indicator::Indicator>>,
|
|
559
585
|
capture: Mutex<Option<FrameSource>>,
|
|
560
|
-
|
|
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>>,
|
|
561
589
|
last_seq: Mutex<u64>,
|
|
562
590
|
control_open: AtomicBool,
|
|
563
591
|
closed: AtomicBool,
|
|
@@ -565,8 +593,35 @@ struct Inner {
|
|
|
565
593
|
/// from the lease as well as from the consumer.
|
|
566
594
|
pipeline: Arc<AtomicBool>,
|
|
567
595
|
events: tokio_mpsc::UnboundedSender<Notice>,
|
|
596
|
+
/// Present only for an encoded source.
|
|
597
|
+
encoded: Mutex<Option<Encoded>>,
|
|
568
598
|
}
|
|
569
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
|
+
|
|
570
625
|
pub struct Session {
|
|
571
626
|
inner: Arc<Inner>,
|
|
572
627
|
}
|
|
@@ -633,6 +688,10 @@ pub fn capabilities() -> serde_json::Value {
|
|
|
633
688
|
},
|
|
634
689
|
"clipboard": { "read": clipboard, "write": clipboard && clipboard::writer_available(), "mime": ["text/plain;charset=utf-8"],
|
|
635
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"] },
|
|
636
695
|
})
|
|
637
696
|
}
|
|
638
697
|
|
|
@@ -687,6 +746,7 @@ pub fn capabilities() -> serde_json::Value {
|
|
|
687
746
|
},
|
|
688
747
|
"clipboard": { "read": true, "write": true, "mime": ["text/plain;charset=utf-8"],
|
|
689
748
|
"maxBytes": clipboard::MAX_CLIPBOARD_BYTES },
|
|
749
|
+
"encoded": { "codecs": ["h264"] },
|
|
690
750
|
})
|
|
691
751
|
}
|
|
692
752
|
|
|
@@ -705,6 +765,17 @@ impl Session {
|
|
|
705
765
|
let wants_control = request.permissions.contains(&Permission::Control);
|
|
706
766
|
let wants_x11 = matches!(request.source, Some(SourceRequest::X11 { .. }));
|
|
707
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
|
+
}
|
|
708
779
|
// Which desktop decides which input path is even available: an X display
|
|
709
780
|
// takes XTest, which cannot reach any other session, while a portal
|
|
710
781
|
// desktop needs kernel input access.
|
|
@@ -738,12 +809,25 @@ impl Session {
|
|
|
738
809
|
let captured = metrics.clone();
|
|
739
810
|
let stop_signal = frame_tx_signal.clone();
|
|
740
811
|
let on_capture_status = move |running: bool, reason: String| {
|
|
741
|
-
if let Ok(mut error) = stopped_error.lock() {
|
|
742
|
-
|
|
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
|
+
}
|
|
743
818
|
stop_signal.send_replace(0);
|
|
744
|
-
let _ = stopped_events.send(Notice {
|
|
745
|
-
|
|
746
|
-
|
|
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
|
+
});
|
|
747
831
|
};
|
|
748
832
|
let capture_status = Arc::new(on_capture_status);
|
|
749
833
|
let frame_events = events.clone();
|
|
@@ -803,7 +887,10 @@ impl Session {
|
|
|
803
887
|
max_fps,
|
|
804
888
|
metrics.clone(),
|
|
805
889
|
sink,
|
|
806
|
-
Box::new({
|
|
890
|
+
Box::new({
|
|
891
|
+
let status = capture_status.clone();
|
|
892
|
+
move |reason| status(false, reason)
|
|
893
|
+
}),
|
|
807
894
|
)
|
|
808
895
|
.map_err(|error| SessionError::new("source", format!("{error:#}")))?,
|
|
809
896
|
_ => {
|
|
@@ -852,7 +939,10 @@ impl Session {
|
|
|
852
939
|
max_fps,
|
|
853
940
|
sink,
|
|
854
941
|
indicator.as_ref().map(crate::indicator::Indicator::pid),
|
|
855
|
-
Box::new({
|
|
942
|
+
Box::new({
|
|
943
|
+
let status = capture_status.clone();
|
|
944
|
+
move |running, reason| status(running, reason)
|
|
945
|
+
}),
|
|
856
946
|
)
|
|
857
947
|
.map_err(|error| SessionError::new("source", format!("{error:#}")))?;
|
|
858
948
|
#[cfg(target_os = "linux")]
|
|
@@ -865,7 +955,10 @@ impl Session {
|
|
|
865
955
|
indicator
|
|
866
956
|
.as_ref()
|
|
867
957
|
.map(crate::indicator::Indicator::position),
|
|
868
|
-
Box::new({
|
|
958
|
+
Box::new({
|
|
959
|
+
let status = capture_status.clone();
|
|
960
|
+
move |reason| status(false, reason)
|
|
961
|
+
}),
|
|
869
962
|
)
|
|
870
963
|
.map_err(|error| SessionError::new("source", format!("{error:#}")))?;
|
|
871
964
|
#[cfg(target_os = "linux")]
|
|
@@ -963,9 +1056,11 @@ impl Session {
|
|
|
963
1056
|
pace_bps: (bitrate_kbps as f64 * 3_000.0).max(20_000_000.0),
|
|
964
1057
|
},
|
|
965
1058
|
peer_events_tx,
|
|
1059
|
+
VideoCodec::Vp9,
|
|
966
1060
|
)
|
|
967
1061
|
.await
|
|
968
1062
|
.map_err(|error| SessionError::new("transport", format!("{error:#}")))?;
|
|
1063
|
+
let peer = Arc::new(peer);
|
|
969
1064
|
|
|
970
1065
|
let generation = 1u64;
|
|
971
1066
|
let geometry = serde_json::json!({
|
|
@@ -985,17 +1080,18 @@ impl Session {
|
|
|
985
1080
|
latest,
|
|
986
1081
|
capture_error,
|
|
987
1082
|
frame_changes,
|
|
988
|
-
peer:
|
|
989
|
-
encoder: Mutex::new(encoder),
|
|
1083
|
+
peer: Mutex::new(Some(peer.clone())),
|
|
1084
|
+
encoder: Mutex::new(Some(encoder)),
|
|
990
1085
|
input: Mutex::new(input),
|
|
991
1086
|
indicator: Mutex::new(indicator),
|
|
992
1087
|
capture: Mutex::new(Some(capture)),
|
|
993
|
-
layout: Mutex::new(layout),
|
|
1088
|
+
layout: Mutex::new(Some(layout)),
|
|
994
1089
|
last_seq: Mutex::new(0),
|
|
995
1090
|
control_open: AtomicBool::new(false),
|
|
996
1091
|
closed: AtomicBool::new(false),
|
|
997
1092
|
pipeline: pipeline.clone(),
|
|
998
1093
|
events: events.clone(),
|
|
1094
|
+
encoded: Mutex::new(None),
|
|
999
1095
|
});
|
|
1000
1096
|
|
|
1001
1097
|
inner.notify(SessionEvent::Description {
|
|
@@ -1008,13 +1104,140 @@ impl Session {
|
|
|
1008
1104
|
first_frame: false,
|
|
1009
1105
|
});
|
|
1010
1106
|
|
|
1011
|
-
spawn_pipeline(&inner, frame_rx, pipeline, max_fps, bitrate_kbps);
|
|
1107
|
+
spawn_pipeline(&inner, peer, frame_rx, pipeline, max_fps, bitrate_kbps);
|
|
1012
1108
|
spawn_peer_events(&inner, peer_events_rx);
|
|
1013
1109
|
spawn_lease(&inner, request.ttl.unwrap_or(Duration::from_secs(3600)));
|
|
1014
1110
|
|
|
1015
1111
|
Ok(Self { inner })
|
|
1016
1112
|
}
|
|
1017
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
|
+
|
|
1018
1241
|
pub fn id(&self) -> &str {
|
|
1019
1242
|
&self.inner.id
|
|
1020
1243
|
}
|
|
@@ -1037,6 +1260,12 @@ impl Session {
|
|
|
1037
1260
|
still_ms: Option<u64>,
|
|
1038
1261
|
timeout_ms: u64,
|
|
1039
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
|
+
}
|
|
1040
1269
|
let mut changes = self.inner.frame_changes.clone();
|
|
1041
1270
|
let deadline = tokio::time::Instant::now() + Duration::from_millis(timeout_ms.min(120_000));
|
|
1042
1271
|
loop {
|
|
@@ -1086,6 +1315,12 @@ impl Session {
|
|
|
1086
1315
|
"view permission is required",
|
|
1087
1316
|
));
|
|
1088
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
|
+
}
|
|
1089
1324
|
if let Some(reason) = lock(&self.inner.capture_error).clone() {
|
|
1090
1325
|
return Err(SessionError::new("stream-stopped", reason));
|
|
1091
1326
|
}
|
|
@@ -1140,7 +1375,32 @@ impl Session {
|
|
|
1140
1375
|
}
|
|
1141
1376
|
|
|
1142
1377
|
pub async fn accept_answer(&self, sdp: String) -> Result<()> {
|
|
1143
|
-
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(())
|
|
1144
1404
|
}
|
|
1145
1405
|
|
|
1146
1406
|
pub async fn add_candidate(
|
|
@@ -1149,8 +1409,7 @@ impl Session {
|
|
|
1149
1409
|
sdp_mid: Option<String>,
|
|
1150
1410
|
sdp_m_line_index: Option<u16>,
|
|
1151
1411
|
) -> Result<()> {
|
|
1152
|
-
self.
|
|
1153
|
-
.peer
|
|
1412
|
+
self.current_peer()?
|
|
1154
1413
|
.add_candidate(candidate, sdp_mid, sdp_m_line_index)
|
|
1155
1414
|
.await
|
|
1156
1415
|
}
|
|
@@ -1211,6 +1470,7 @@ impl Inner {
|
|
|
1211
1470
|
}
|
|
1212
1471
|
self.control_open.store(false, Ordering::SeqCst);
|
|
1213
1472
|
self.pipeline.store(false, Ordering::SeqCst);
|
|
1473
|
+
drop(lock(&self.encoded).take());
|
|
1214
1474
|
if let Ok(mut indicator) = self.indicator.lock() {
|
|
1215
1475
|
indicator.take();
|
|
1216
1476
|
}
|
|
@@ -1222,14 +1482,16 @@ impl Inner {
|
|
|
1222
1482
|
}
|
|
1223
1483
|
*input = None;
|
|
1224
1484
|
}
|
|
1225
|
-
let
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
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
|
+
}
|
|
1233
1495
|
// Dropping the capture stops the PipeWire stream and joins its thread,
|
|
1234
1496
|
// which is what releases the compositor's consent for this session. It
|
|
1235
1497
|
// has to happen synchronously, not when the last handle happens to fall
|
|
@@ -1256,7 +1518,10 @@ impl Inner {
|
|
|
1256
1518
|
}
|
|
1257
1519
|
|
|
1258
1520
|
fn clipboard_refusal(&self) -> Option<(&'static str, &'static str)> {
|
|
1259
|
-
if
|
|
1521
|
+
if matches!(
|
|
1522
|
+
self.source.source_type.as_deref(),
|
|
1523
|
+
Some("x11-root") | Some("encoded")
|
|
1524
|
+
) || !wayland_clipboard_available()
|
|
1260
1525
|
{
|
|
1261
1526
|
return Some((
|
|
1262
1527
|
"clipboard-unsupported",
|
|
@@ -1279,9 +1544,14 @@ impl Inner {
|
|
|
1279
1544
|
|
|
1280
1545
|
fn reply(&self, payload: &ControlReply<'_>) -> Result<()> {
|
|
1281
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
|
+
};
|
|
1282
1553
|
// The channel is asynchronous; a failure here means the peer is gone,
|
|
1283
1554
|
// which the connection-state handler closes anyway.
|
|
1284
|
-
let peer = self.peer.clone();
|
|
1285
1555
|
tokio::spawn(async move {
|
|
1286
1556
|
let _ = peer.send_control(&text).await;
|
|
1287
1557
|
});
|
|
@@ -1318,7 +1588,7 @@ impl Inner {
|
|
|
1318
1588
|
*last = seq;
|
|
1319
1589
|
}
|
|
1320
1590
|
let feedback = match &message {
|
|
1321
|
-
ControlMessage::Pointer { phase, x, y, .. } => {
|
|
1591
|
+
ControlMessage::Pointer { phase, x, y, .. } if lock(&self.encoded).is_none() => {
|
|
1322
1592
|
let (x, y) = to_source_pixels(*x, *y, self.encoded_size(), &self.source);
|
|
1323
1593
|
Some((
|
|
1324
1594
|
if matches!(phase, PointerPhase::Down) {
|
|
@@ -1334,6 +1604,13 @@ impl Inner {
|
|
|
1334
1604
|
ControlMessage::Key { down: true, .. } => Some(('T', -1, -1)),
|
|
1335
1605
|
_ => None,
|
|
1336
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
|
+
}
|
|
1337
1614
|
let outcome = match message {
|
|
1338
1615
|
ControlMessage::Pointer {
|
|
1339
1616
|
phase,
|
|
@@ -1386,6 +1663,41 @@ impl Inner {
|
|
|
1386
1663
|
}
|
|
1387
1664
|
}
|
|
1388
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
|
+
|
|
1389
1701
|
fn with_input<T>(
|
|
1390
1702
|
&self,
|
|
1391
1703
|
action: impl FnOnce(&mut InputTarget) -> Result<T>,
|
|
@@ -1462,10 +1774,13 @@ impl Inner {
|
|
|
1462
1774
|
}
|
|
1463
1775
|
|
|
1464
1776
|
let stroke = {
|
|
1465
|
-
let
|
|
1777
|
+
let held = self
|
|
1466
1778
|
.layout
|
|
1467
1779
|
.lock()
|
|
1468
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")))?;
|
|
1469
1784
|
if let Some(name) = name {
|
|
1470
1785
|
keymap::named_key(&name)
|
|
1471
1786
|
.ok_or(("text-unsupported", format!("unknown key {name}")))?
|
|
@@ -1512,12 +1827,15 @@ impl Inner {
|
|
|
1512
1827
|
}
|
|
1513
1828
|
#[cfg(target_os = "linux")]
|
|
1514
1829
|
{
|
|
1515
|
-
let
|
|
1830
|
+
let held = self
|
|
1516
1831
|
.layout
|
|
1517
1832
|
.lock()
|
|
1518
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")))?;
|
|
1519
1837
|
let (plan, unreachable) = layout.plan_text(text);
|
|
1520
|
-
drop(
|
|
1838
|
+
drop(held);
|
|
1521
1839
|
if !unreachable.is_empty() {
|
|
1522
1840
|
return Err((
|
|
1523
1841
|
"text-unsupported",
|
|
@@ -1774,6 +2092,7 @@ enum Pass {
|
|
|
1774
2092
|
/// the desktop goes still the last frame is coded once more, sharp.
|
|
1775
2093
|
fn spawn_pipeline(
|
|
1776
2094
|
inner: &Arc<Inner>,
|
|
2095
|
+
peer: Arc<VideoPeer>,
|
|
1777
2096
|
frame_rx: FrameReceiver,
|
|
1778
2097
|
running: Arc<AtomicBool>,
|
|
1779
2098
|
max_fps: u32,
|
|
@@ -1802,7 +2121,7 @@ fn spawn_pipeline(
|
|
|
1802
2121
|
let slot = last_sent.map_or(Duration::ZERO, |at| {
|
|
1803
2122
|
interval.saturating_sub(now.duration_since(at))
|
|
1804
2123
|
});
|
|
1805
|
-
let connected =
|
|
2124
|
+
let connected = peer.is_connected();
|
|
1806
2125
|
let wait = if !connected {
|
|
1807
2126
|
// Nothing sent before the transport connects arrives; the
|
|
1808
2127
|
// first frame after it is a key frame.
|
|
@@ -1838,15 +2157,17 @@ fn spawn_pipeline(
|
|
|
1838
2157
|
if !running.load(Ordering::Relaxed) {
|
|
1839
2158
|
break;
|
|
1840
2159
|
}
|
|
1841
|
-
if
|
|
2160
|
+
if peer.take_keyframe_request() || !connected {
|
|
1842
2161
|
keyframe = true;
|
|
1843
2162
|
}
|
|
1844
|
-
let reports =
|
|
2163
|
+
let reports = peer.take_loss_reports();
|
|
1845
2164
|
if let Some(kbps) = rate.update(&reports, Instant::now()) {
|
|
1846
|
-
let applied = inner
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
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
|
+
});
|
|
1850
2171
|
if matches!(applied, Ok(Ok(()))) {
|
|
1851
2172
|
if let Ok(mut m) = inner.metrics.lock() {
|
|
1852
2173
|
m.target_kbps = kbps;
|
|
@@ -1877,10 +2198,13 @@ fn spawn_pipeline(
|
|
|
1877
2198
|
|
|
1878
2199
|
let started = Instant::now();
|
|
1879
2200
|
let packet = {
|
|
1880
|
-
let mut
|
|
1881
|
-
Ok(
|
|
2201
|
+
let mut held = match inner.encoder.lock() {
|
|
2202
|
+
Ok(held) => held,
|
|
1882
2203
|
Err(_) => break,
|
|
1883
2204
|
};
|
|
2205
|
+
let Some(encoder) = held.as_mut() else {
|
|
2206
|
+
break;
|
|
2207
|
+
};
|
|
1884
2208
|
match pass {
|
|
1885
2209
|
Pass::Motion { keyframe } => encoder.encode(frame, keyframe),
|
|
1886
2210
|
Pass::Refine => encoder.refine(frame),
|
|
@@ -1931,7 +2255,6 @@ fn spawn_pipeline(
|
|
|
1931
2255
|
Pass::Keepalive => {}
|
|
1932
2256
|
}
|
|
1933
2257
|
last_sent = Some(now);
|
|
1934
|
-
let peer = inner.peer.clone();
|
|
1935
2258
|
if let Err(error) =
|
|
1936
2259
|
handle.block_on(peer.send_frame(&packet.data, packet.keyframe, now))
|
|
1937
2260
|
{
|
|
@@ -1953,6 +2276,113 @@ fn spawn_pipeline(
|
|
|
1953
2276
|
.ok();
|
|
1954
2277
|
}
|
|
1955
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
|
+
|
|
1956
2386
|
fn spawn_peer_events(inner: &Arc<Inner>, mut events: tokio_mpsc::UnboundedReceiver<PeerEvent>) {
|
|
1957
2387
|
let inner = inner.clone();
|
|
1958
2388
|
tokio::spawn(async move {
|
|
@@ -1975,6 +2405,16 @@ fn spawn_peer_events(inner: &Arc<Inner>, mut events: tokio_mpsc::UnboundedReceiv
|
|
|
1975
2405
|
}
|
|
1976
2406
|
PeerEvent::State(state) => {
|
|
1977
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
|
+
}
|
|
1978
2418
|
// ICE loss need not close SCTP. Never leave a drag or chord
|
|
1979
2419
|
// held while waiting for that independent notification.
|
|
1980
2420
|
if matches!(state, State::Disconnected | State::Failed | State::Closed) {
|
|
@@ -2021,6 +2461,11 @@ fn spawn_peer_events(inner: &Arc<Inner>, mut events: tokio_mpsc::UnboundedReceiv
|
|
|
2021
2461
|
}
|
|
2022
2462
|
}
|
|
2023
2463
|
}
|
|
2464
|
+
PeerEvent::KeyframeRequest => {
|
|
2465
|
+
inner.notify(SessionEvent::KeyframeRequest {
|
|
2466
|
+
generation: inner.generation,
|
|
2467
|
+
});
|
|
2468
|
+
}
|
|
2024
2469
|
PeerEvent::ControlMessage(text) => {
|
|
2025
2470
|
match serde_json::from_str::<ControlMessage>(&text) {
|
|
2026
2471
|
Ok(message) => inner.apply(message),
|
|
@@ -2082,6 +2527,71 @@ fn opaque_id() -> String {
|
|
|
2082
2527
|
mod tests {
|
|
2083
2528
|
use super::*;
|
|
2084
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
|
+
|
|
2085
2595
|
#[test]
|
|
2086
2596
|
fn tile_damage_at_origin_does_not_underflow_or_hide_other_tiles() {
|
|
2087
2597
|
assert_eq!(
|
|
@@ -2144,6 +2654,7 @@ mod tests {
|
|
|
2144
2654
|
pace_bps: 20_000_000.0,
|
|
2145
2655
|
},
|
|
2146
2656
|
peer_events,
|
|
2657
|
+
VideoCodec::Vp9,
|
|
2147
2658
|
)
|
|
2148
2659
|
.await
|
|
2149
2660
|
.expect("a peer connection builds without a network");
|
|
@@ -2172,8 +2683,8 @@ mod tests {
|
|
|
2172
2683
|
latest: Arc::new(Mutex::new(None)),
|
|
2173
2684
|
capture_error: Arc::new(Mutex::new(None)),
|
|
2174
2685
|
frame_changes: tokio::sync::watch::channel(0).1,
|
|
2175
|
-
peer: Arc::new(peer),
|
|
2176
|
-
encoder: Mutex::new(Encoder::new(64, 64, 1000, 30, 1).expect("an encoder")),
|
|
2686
|
+
peer: Mutex::new(Some(Arc::new(peer))),
|
|
2687
|
+
encoder: Mutex::new(Some(Encoder::new(64, 64, 1000, 30, 1).expect("an encoder"))),
|
|
2177
2688
|
input: Mutex::new(Some(InputTarget {
|
|
2178
2689
|
applier: Applier::Recording(recorded.clone()),
|
|
2179
2690
|
held: HeldState::default(),
|
|
@@ -2184,12 +2695,13 @@ mod tests {
|
|
|
2184
2695
|
})),
|
|
2185
2696
|
capture: Mutex::new(None),
|
|
2186
2697
|
indicator: Mutex::new(None),
|
|
2187
|
-
layout: Mutex::new(Layout::from_environment().expect("a keymap")),
|
|
2698
|
+
layout: Mutex::new(Some(Layout::from_environment().expect("a keymap"))),
|
|
2188
2699
|
last_seq: Mutex::new(0),
|
|
2189
2700
|
control_open: AtomicBool::new(true),
|
|
2190
2701
|
closed: AtomicBool::new(false),
|
|
2191
2702
|
pipeline: Arc::new(AtomicBool::new(true)),
|
|
2192
2703
|
events,
|
|
2704
|
+
encoded: Mutex::new(None),
|
|
2193
2705
|
});
|
|
2194
2706
|
(inner, recorded)
|
|
2195
2707
|
}
|
|
@@ -2474,12 +2986,22 @@ mod tests {
|
|
|
2474
2986
|
async fn a_stopped_capture_never_serves_the_last_frame() {
|
|
2475
2987
|
let (events, _) = tokio_mpsc::unbounded_channel();
|
|
2476
2988
|
let (mut inner, _) = test_inner(events).await;
|
|
2477
|
-
Arc::get_mut(&mut inner)
|
|
2989
|
+
Arc::get_mut(&mut inner)
|
|
2990
|
+
.unwrap()
|
|
2991
|
+
.permissions
|
|
2992
|
+
.push(Permission::View);
|
|
2478
2993
|
let session = Session { inner };
|
|
2479
|
-
*lock(&session.inner.latest) =
|
|
2994
|
+
*lock(&session.inner.latest) =
|
|
2995
|
+
Some((1, Instant::now(), 2, 2, vec![0; 16], vec![1], vec![]));
|
|
2480
2996
|
*lock(&session.inner.capture_error) = Some(String::from("SCStreamErrorDomain -3821"));
|
|
2481
|
-
assert_eq!(
|
|
2482
|
-
|
|
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
|
+
);
|
|
2483
3005
|
}
|
|
2484
3006
|
|
|
2485
3007
|
#[tokio::test]
|
|
@@ -2487,7 +3009,14 @@ mod tests {
|
|
|
2487
3009
|
let (events, mut received) = tokio_mpsc::unbounded_channel();
|
|
2488
3010
|
let (inner, _recorded) = test_inner(events).await;
|
|
2489
3011
|
let (frame_tx, frame_rx) = latest_frame();
|
|
2490
|
-
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
|
+
);
|
|
2491
3020
|
|
|
2492
3021
|
// The encoder is 64x64; a 32x32 frame is the dimension mismatch that
|
|
2493
3022
|
// used to be counted and dropped behind a permanently black picture.
|
|
@@ -2519,6 +3048,183 @@ mod tests {
|
|
|
2519
3048
|
);
|
|
2520
3049
|
}
|
|
2521
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
|
+
|
|
2522
3228
|
#[tokio::test]
|
|
2523
3229
|
async fn opening_without_view_permission_is_refused_before_capture() {
|
|
2524
3230
|
let (events, _received) = tokio_mpsc::unbounded_channel();
|
|
@@ -2602,7 +3308,14 @@ mod tests {
|
|
|
2602
3308
|
let (events, _events_rx) = tokio_mpsc::unbounded_channel();
|
|
2603
3309
|
let (inner, _recorded) = test_inner(events).await;
|
|
2604
3310
|
let (frame_tx, frame_rx) = latest_frame();
|
|
2605
|
-
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
|
+
);
|
|
2606
3319
|
|
|
2607
3320
|
// 40 frames over 200 ms is 200 fps; the requested 20 fps caps what leaves.
|
|
2608
3321
|
for _ in 0..40 {
|