@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.
@@ -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::{ControlMessage, ControlReply, Permission, PointerPhase, SourceRequest};
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: &Arc<Mutex<T>>) -> std::sync::MutexGuard<'_, T> {
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 { reason: String },
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
- peer: Arc<VideoPeer>,
556
- encoder: Mutex<Encoder>,
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
- layout: Mutex<Layout>,
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() { *error = if running { None } else { Some(reason.clone()) }; }
742
- if let Ok(mut frame) = stopped_latest.lock() { *frame = None; }
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 { session_id: stopped_id.clone(),
745
- event: if running { SessionEvent::State { capture: "streaming", transport: String::from("connected"), first_frame: false } }
746
- else { SessionEvent::CaptureStopped { reason } } });
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({ let status = capture_status.clone(); move |reason| status(false, reason) }),
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({ let status = capture_status.clone(); move |running, reason| status(running, reason) }),
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({ let status = capture_status.clone(); move |reason| status(false, reason) }),
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: Arc::new(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.inner.peer.accept_answer(sdp).await.map(|_applied| ())
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.inner
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 _ = self
1226
- .peer
1227
- .send_control(
1228
- &serde_json::to_string(&ControlReply::Revoked { reason })
1229
- .unwrap_or_else(|_| String::from(r#"{"kind":"revoked","reason":"closed"}"#)),
1230
- )
1231
- .await;
1232
- self.peer.close().await;
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 self.source.source_type.as_deref() == Some("x11-root") || !wayland_clipboard_available()
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 layout = self
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 layout = self
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(layout);
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 = inner.peer.is_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 inner.peer.take_keyframe_request() || !connected {
2160
+ if peer.take_keyframe_request() || !connected {
1842
2161
  keyframe = true;
1843
2162
  }
1844
- let reports = inner.peer.take_loss_reports();
2163
+ let reports = peer.take_loss_reports();
1845
2164
  if let Some(kbps) = rate.update(&reports, Instant::now()) {
1846
- let applied = inner
1847
- .encoder
1848
- .lock()
1849
- .map(|mut encoder| encoder.set_bitrate(kbps));
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 encoder = match inner.encoder.lock() {
1881
- Ok(encoder) => encoder,
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).unwrap().permissions.push(Permission::View);
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) = Some((1, Instant::now(), 2, 2, vec![0;16], vec![1], vec![]));
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!(session.frame(None, "", None).unwrap_err().code, "stream-stopped");
2482
- assert_eq!(session.wait_frame(None, None, 10).await.unwrap_err().code, "stream-stopped");
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(&inner, frame_rx, Arc::new(AtomicBool::new(true)), 30, 1000);
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(&inner, frame_rx, Arc::new(AtomicBool::new(true)), 20, 1000);
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 {