1use crate::channels::{ChannelKind, ChannelSpec};
9use crate::control::{encode_line, FromPilot, ToPilot};
10use crate::packer::{self, FrameParams};
11use bytes::Bytes;
12use seyd_qos::abr::{AbrController, Sample as AbrSample};
13use seyd_qos::simulcast::LayerSelector;
14use seyd_qos::Profile;
15use seyd_transport::{Endpoint, SendError, Session};
16use seyd_wire::v2::{ChunkHeader, FrameMeta};
17use std::collections::{HashMap, VecDeque};
18use std::sync::atomic::{AtomicU16, AtomicU64, Ordering};
19use std::sync::{Arc, Mutex, RwLock};
20use std::time::{Duration, Instant};
21use tokio::sync::{mpsc, Notify};
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum Role {
25 Driver,
26 Observer,
27}
28
29impl Role {
30 pub fn as_str(&self) -> &'static str {
31 match self {
32 Role::Driver => "driver",
33 Role::Observer => "observer",
34 }
35 }
36}
37
38#[derive(Debug, Clone)]
40pub enum Event {
41 SessionStarted {
42 signal_id: String,
43 role: Role,
44 path_label: String,
45 },
46 SessionEnded {
47 signal_id: String,
48 reason: String,
49 },
50 Command {
52 channel: u8,
53 payload: Bytes,
54 },
55 RequestedConfig(serde_json::Value),
57 RecoveryRequest {
59 channel: u8,
60 kind: &'static str,
61 reason: &'static str,
62 },
63 LayerChanged {
67 channel: u8,
68 layer: u8,
69 name: String,
70 reason: &'static str,
71 },
72}
73
74pub struct VideoFrame {
76 pub data: Bytes,
77 pub keyframe: bool,
78 pub capture_ts_us: u64,
79}
80
81#[derive(Clone)]
82pub struct EngineConfig {
83 pub channels: Vec<ChannelSpec>,
84 pub profile: &'static Profile,
85 pub max_sessions: usize,
86 pub recovery_ladder: bool,
90 pub chunk_len: u16,
91}
92
93#[derive(Default, Debug)]
94pub struct Counters {
95 pub frames_in: AtomicU64,
96 pub frames_sent: AtomicU64,
97 pub frames_dropped_backlog: AtomicU64,
98 pub frames_skipped_stale: AtomicU64,
99 pub frames_dropped_inactive_layer: AtomicU64,
103 pub keyframes_requested: AtomicU64,
104 pub chunks_sent: AtomicU64,
105 pub parity_sent: AtomicU64,
106 pub bytes_sent: AtomicU64,
107}
108
109struct SessionState {
110 transport: Arc<Session>,
111 signal_id: String,
112 role: Role,
113 lines: mpsc::UnboundedSender<String>,
114 skip_until_key: std::sync::atomic::AtomicBool,
117 skip_deadline_ms: std::sync::atomic::AtomicU64,
120 pilot: Mutex<PilotSample>,
122 last_keyframe: Mutex<Option<(Instant, usize)>>,
126 backlog_since: Mutex<Option<Instant>>,
129}
130
131#[derive(Default)]
132struct PilotSample {
133 pairs: VecDeque<(Instant, u64, u64)>,
137 loss_pct: Option<f64>,
138 loss_pilot_pct: Option<f64>,
140 last_incomplete: Option<(u64, u64)>,
141 incomplete_delta: u64,
143 prev_lost_packets: u64,
145}
146
147struct AbrView {
148 bitrate_kbps: u32,
149 fec: (u32, u32),
150 reason: &'static str,
151 loss_pct: Option<f64>,
152 loss_pilot_pct: Option<f64>,
153}
154
155struct AbrState {
157 controllers: HashMap<u8, AbrController>,
158 fec: (u32, u32),
160 bitrate_kbps: u32,
161 reason: &'static str,
162 prev_backlog: u64,
163 loss_pct: Option<f64>,
164 loss_pilot_pct: Option<f64>,
165 last_keyframe_sent: Option<Instant>,
166}
167
168struct Pending {
169 role: Role,
170 since: Instant,
171}
172
173struct ChannelLayers {
175 selector: LayerSelector,
176 active: u8,
178 pending: Option<u8>,
181}
182
183fn windowed_loss_pct(span: Duration, d_sent: u64, d_rx: u64) -> Option<f64> {
192 if span < Duration::from_secs(3) || d_sent < 50 {
193 return None;
194 }
195 Some(((1.0 - d_rx as f64 / d_sent as f64) * 100.0).clamp(0.0, 100.0))
196}
197
198struct FrameQueue {
210 frames: VecDeque<(u8, VideoFrame)>,
211 skip_until_key: bool,
212 resume_at: Option<Instant>,
215}
216
217const MAX_QUEUED_FRAMES: usize = 6;
218
219const KEYFRAME_FLUSH_FRAMES: usize = MAX_QUEUED_FRAMES / 2;
223
224impl FrameQueue {
240 fn resume_due(&self) -> bool {
247 self.resume_at.is_some_and(|t| Instant::now() >= t)
248 }
249}
250
251fn keyframe_flushes_queue(queued: usize, skip_until_key: bool) -> bool {
252 skip_until_key || queued >= KEYFRAME_FLUSH_FRAMES
253}
254
255fn backlog_exceeds(queued: usize, threshold: usize, keyframe_allowance: usize) -> bool {
267 queued.saturating_sub(keyframe_allowance) > threshold
268}
269
270const BACKLOG_SUSTAIN: Duration = Duration::from_millis(300);
288
289fn backlog_sustained(
293 over: bool,
294 since: &mut Option<Instant>,
295 now: Instant,
296 sustain: Duration,
297) -> bool {
298 if !over {
299 *since = None;
300 return false;
301 }
302 let start = *since.get_or_insert(now);
303 now.duration_since(start) >= sustain
304}
305
306fn keyframe_allowance(last_keyframe: Option<(Duration, usize)>, bitrate_kbps: u32) -> usize {
317 let Some((since, wire_bytes)) = last_keyframe else {
318 return 0;
319 };
320 let drain_ms = wire_bytes as u64 * 8 / u64::from(bitrate_kbps.max(1));
321 if since <= Duration::from_millis(drain_ms * 2) {
322 wire_bytes
323 } else {
324 0
325 }
326}
327
328#[derive(Clone, Copy, PartialEq, Eq, Debug)]
338pub enum Recovery {
339 Ltr,
342 IntraRefresh,
345 Idr,
348}
349
350impl Recovery {
351 fn as_str(self) -> &'static str {
352 match self {
353 Recovery::Ltr => "ltr",
354 Recovery::IntraRefresh => "intra_refresh",
355 Recovery::Idr => "idr",
356 }
357 }
358
359 fn escalate(self) -> Self {
362 match self {
363 Recovery::Ltr => Recovery::IntraRefresh,
364 Recovery::IntraRefresh | Recovery::Idr => Recovery::Idr,
365 }
366 }
367
368 fn yields_keyframe(self) -> bool {
372 matches!(self, Recovery::Idr)
373 }
374}
375
376const RECOVERY_ESCALATE_WINDOW: Duration = Duration::from_millis(2_000);
379
380#[derive(Clone, Copy)]
381struct RecoveryState {
382 last: Instant,
384 rung: Recovery,
385 rung_since: Instant,
391}
392
393struct Inner {
394 cfg: EngineConfig,
395 profile: RwLock<&'static Profile>,
396 sessions: Mutex<HashMap<u64, Arc<SessionState>>>,
397 pending: Mutex<HashMap<String, Pending>>,
398 frame_ids: HashMap<u8, AtomicU16>,
399 seqs: HashMap<u8, AtomicU16>,
400 counters: Counters,
401 events: mpsc::Sender<Event>,
402 last_recovery: Mutex<HashMap<u8, RecoveryState>>,
404 queue: Mutex<FrameQueue>,
405 slot_notify: Notify,
406 epoch: Instant,
407 abr: Mutex<AbrState>,
408 simulcast: Mutex<HashMap<u8, ChannelLayers>>,
411}
412
413impl Inner {
414 fn publisher_max_kbps(&self, channel: u8) -> u32 {
418 self.cfg
419 .channels
420 .iter()
421 .filter(|c| c.kind == ChannelKind::Video && (channel == 0 || c.id == channel))
422 .map(|c| c.max_bitrate_kbps)
423 .filter(|&k| k > 0)
424 .min()
425 .unwrap_or(0)
426 }
427
428 fn ceiling_kbps(&self, profile: &Profile, channel: u8) -> u32 {
429 seyd_qos::abr::effective_ceiling_kbps(profile, self.publisher_max_kbps(channel))
430 }
431
432 fn publisher_config(&self, profile: &Profile, channel: u8, reason: &str) -> serde_json::Value {
435 let mut cfg = profile.publisher_config(channel, reason);
436 cfg["maxBitrateKbps"] = serde_json::json!(self.ceiling_kbps(profile, channel));
437 cfg
438 }
439}
440
441#[derive(Clone)]
442pub struct Engine {
443 inner: Arc<Inner>,
444}
445
446impl Engine {
447 pub fn new(cfg: EngineConfig) -> (Engine, mpsc::Receiver<Event>) {
448 let (events, rx) = mpsc::channel(256);
449 let frame_ids = cfg
450 .channels
451 .iter()
452 .map(|c| (c.id, AtomicU16::new(0)))
453 .collect();
454 let seqs = cfg
455 .channels
456 .iter()
457 .map(|c| (c.id, AtomicU16::new(0)))
458 .collect();
459 let profile = cfg.profile;
460 let simulcast: HashMap<u8, ChannelLayers> = cfg
463 .channels
464 .iter()
465 .filter(|c| c.kind == ChannelKind::Video && c.layers.len() > 1)
466 .filter_map(|c| {
467 LayerSelector::new(
468 c.layers.clone(),
469 seyd_qos::abr::effective_ceiling_kbps(profile, c.max_bitrate_kbps),
470 )
471 .map(|selector| {
472 let active = selector.current();
473 (
474 c.id,
475 ChannelLayers {
476 selector,
477 active,
478 pending: None,
479 },
480 )
481 })
482 })
483 .collect();
484 let initial_ceiling_kbps = seyd_qos::abr::effective_ceiling_kbps(
485 profile,
486 cfg.channels
487 .iter()
488 .filter(|c| c.kind == ChannelKind::Video)
489 .map(|c| c.max_bitrate_kbps)
490 .filter(|&k| k > 0)
491 .min()
492 .unwrap_or(0),
493 );
494 let inner = Arc::new(Inner {
495 profile: RwLock::new(cfg.profile),
496 cfg,
497 sessions: Mutex::new(HashMap::new()),
498 pending: Mutex::new(HashMap::new()),
499 frame_ids,
500 seqs,
501 counters: Counters::default(),
502 events,
503 last_recovery: Mutex::new(HashMap::new()),
504 queue: Mutex::new(FrameQueue {
505 frames: VecDeque::new(),
506 skip_until_key: false,
507 resume_at: None,
508 }),
509 slot_notify: Notify::new(),
510 epoch: Instant::now(),
511 simulcast: Mutex::new(simulcast),
512 abr: Mutex::new(AbrState {
513 controllers: HashMap::new(),
514 fec: (profile.fec_delta_pct, profile.fec_key_pct),
515 bitrate_kbps: initial_ceiling_kbps,
516 reason: "steady",
517 prev_backlog: 0,
518 loss_pct: None,
519 loss_pilot_pct: None,
520 last_keyframe_sent: None,
521 }),
522 });
523 let engine = Engine { inner };
524 tokio::spawn(engine.clone().frame_sender());
525 tokio::spawn(engine.clone().abr_loop());
526 (engine, rx)
527 }
528
529 pub fn channels(&self) -> &[ChannelSpec] {
530 &self.inner.cfg.channels
531 }
532 pub fn channel_by_name(&self, name: &str) -> Option<&ChannelSpec> {
533 self.inner.cfg.channels.iter().find(|c| c.name == name)
534 }
535 pub fn counters(&self) -> &Counters {
536 &self.inner.counters
537 }
538 pub fn profile(&self) -> &'static Profile {
539 *self.inner.profile.read().unwrap()
540 }
541 pub fn ceiling_kbps(&self, channel: u8) -> u32 {
544 self.inner.ceiling_kbps(self.profile(), channel)
545 }
546 pub fn publisher_config(&self, channel: u8, reason: &str) -> serde_json::Value {
549 self.inner.publisher_config(self.profile(), channel, reason)
550 }
551 pub fn now_us(&self) -> u64 {
552 self.inner.epoch.elapsed().as_micros() as u64
553 }
554 pub fn session_count(&self) -> usize {
555 self.inner.sessions.lock().unwrap().len()
556 }
557 pub fn signal_session_ids(&self) -> Vec<String> {
558 self.inner
559 .sessions
560 .lock()
561 .unwrap()
562 .values()
563 .map(|s| s.signal_id.clone())
564 .collect()
565 }
566
567 pub fn expect_pilot(&self, signal_id: &str, role: Role) {
570 let mut p = self.inner.pending.lock().unwrap();
571 p.retain(|_, v| v.since.elapsed() < Duration::from_secs(30));
572 p.insert(
573 signal_id.to_string(),
574 Pending {
575 role,
576 since: Instant::now(),
577 },
578 );
579 }
580
581 pub fn revoke(&self, signal_id: &str, reason: &str) {
583 let victim = self
584 .inner
585 .sessions
586 .lock()
587 .unwrap()
588 .values()
589 .find(|s| s.signal_id == signal_id)
590 .cloned();
591 if let Some(s) = victim {
592 s.transport.close(0, reason);
593 }
594 }
595
596 pub fn set_profile(&self, profile: &'static Profile, reason: &str) {
598 *self.inner.profile.write().unwrap() = profile;
599 {
600 let mut abr = self.inner.abr.lock().unwrap();
603 abr.controllers.clear();
604 abr.fec = (profile.fec_delta_pct, profile.fec_key_pct);
605 abr.bitrate_kbps = self.inner.ceiling_kbps(profile, 0);
606 abr.reason = "steady";
607 }
608 for c in self
609 .inner
610 .cfg
611 .channels
612 .iter()
613 .filter(|c| c.kind == ChannelKind::Video)
614 {
615 let _ = self.inner.events.try_send(Event::RequestedConfig(
616 self.inner.publisher_config(profile, c.id, reason),
617 ));
618 }
619 }
620
621 pub fn push_video(&self, channel: u8, frame: VideoFrame) {
627 self.push_video_layer(channel, 0, frame)
628 }
629
630 pub fn push_video_layer(&self, channel: u8, layer: u8, frame: VideoFrame) {
635 {
636 let mut sim = self.inner.simulcast.lock().unwrap();
637 if let Some(st) = sim.get_mut(&channel) {
638 if st.pending == Some(layer) && frame.keyframe {
642 st.active = layer;
643 st.pending = None;
644 }
645 if st.active != layer {
646 self.inner
647 .counters
648 .frames_dropped_inactive_layer
649 .fetch_add(1, Ordering::Relaxed);
650 return;
651 }
652 }
653 }
654 self.inner
655 .counters
656 .frames_in
657 .fetch_add(1, Ordering::Relaxed);
658 let mut q = self.inner.queue.lock().unwrap();
659 if frame.keyframe {
660 if keyframe_flushes_queue(q.frames.len(), q.skip_until_key) {
661 let flushed = q.frames.len();
662 q.frames.clear();
663 if flushed > 0 {
664 self.inner
665 .counters
666 .frames_skipped_stale
667 .fetch_add(flushed as u64, Ordering::Relaxed);
668 }
669 }
670 q.skip_until_key = false;
671 q.resume_at = None;
672 } else if (q.skip_until_key && !q.resume_due()) || q.frames.len() >= MAX_QUEUED_FRAMES {
673 let first = !q.skip_until_key;
674 q.skip_until_key = true;
675 self.inner
676 .counters
677 .frames_skipped_stale
678 .fetch_add(1, Ordering::Relaxed);
679 drop(q);
680 if first {
681 let asked = self.request_recovery(channel, "sender-backlog");
682 self.arm_resume(asked);
683 }
684 return;
685 }
686 q.frames.push_back((channel, frame));
687 drop(q);
688 self.inner.slot_notify.notify_one();
689 }
690
691 pub fn push_message(&self, channel: u8, payload: &[u8]) {
693 let seq = match self.inner.seqs.get(&channel) {
694 Some(s) => s.fetch_add(1, Ordering::Relaxed),
695 None => return,
696 };
697 let dg = packer::pack_message(channel, seq, self.now_us() as u32, payload);
698 self.inner
702 .counters
703 .chunks_sent
704 .fetch_add(1, Ordering::Relaxed);
705 self.inner
706 .counters
707 .bytes_sent
708 .fetch_add(dg.len() as u64, Ordering::Relaxed);
709 for s in self.inner.sessions.lock().unwrap().values() {
710 let _ = s.transport.send_datagram(dg.clone());
711 }
712 }
713
714 pub async fn accept_loop(self, endpoint: Arc<Endpoint>) {
716 while let Some(session) = endpoint.accept().await {
717 tokio::spawn(self.clone().run_session(Arc::new(session)));
718 }
719 }
720
721 pub fn serve(&self, session: Session) {
725 tokio::spawn(self.clone().run_session(Arc::new(session)));
726 }
727
728 async fn frame_sender(self) {
729 loop {
730 let next = self.inner.queue.lock().unwrap().frames.pop_front();
731 match next {
732 Some((channel, frame)) => self.send_frame(channel, frame).await,
733 None => self.inner.slot_notify.notified().await,
734 }
735 }
736 }
737
738 async fn send_frame(&self, channel: u8, frame: VideoFrame) {
739 let sessions: Vec<Arc<SessionState>> = self
740 .inner
741 .sessions
742 .lock()
743 .unwrap()
744 .values()
745 .cloned()
746 .collect();
747 let frame_id = match self.inner.frame_ids.get(&channel) {
748 Some(f) => f.fetch_add(1, Ordering::Relaxed),
749 None => return,
750 };
751 if sessions.is_empty() {
752 return;
753 }
754 let profile = self.profile();
755 let ((fec_delta, fec_key), bitrate_kbps) = {
759 let abr = self.inner.abr.lock().unwrap();
760 (abr.fec, abr.bitrate_kbps)
761 };
762 let fec_pct = if frame.keyframe { fec_key } else { fec_delta };
763 let fps = self
764 .inner
765 .cfg
766 .channels
767 .iter()
768 .find(|c| c.id == channel)
769 .map(|c| c.fps)
770 .filter(|f| *f > 0)
771 .unwrap_or(30);
772 let pack = packer::pack_video(
773 FrameParams {
774 channel_id: channel,
775 frame_id,
776 keyframe: frame.keyframe,
777 fec_pct,
778 chunk_len: self.inner.cfg.chunk_len,
779 send_ts: self.now_us() as u32,
780 meta: Some(FrameMeta {
781 capture_ts_us: frame.capture_ts_us,
782 seq_in_gop: 0,
783 }),
784 },
785 &frame.data,
786 );
787 let wire_bytes: usize = pack.chunks.iter().map(|c| c.len()).sum();
788 let threshold =
789 profile.drop_threshold_bytes_at(self.inner.ceiling_kbps(profile, channel), fps);
790 let mut sent_any = false;
791 tracing::trace!(
792 frame_id,
793 keyframe = frame.keyframe,
794 bytes = frame.data.len(),
795 wire_bytes,
796 chunks = pack.chunks.len(),
797 "frame"
798 );
799
800 for s in &sessions {
801 let space = s.transport.send_buffer_space();
806 let queued = s.transport.send_buffer_queued();
807 if frame.keyframe {
808 s.skip_until_key.store(false, Ordering::Relaxed);
809 s.skip_deadline_ms.store(0, Ordering::Relaxed);
810 } else if s.skip_until_key.load(Ordering::Relaxed)
811 && self.now_ms() < s.skip_deadline_ms.load(Ordering::Relaxed)
812 {
813 self.inner
814 .counters
815 .frames_dropped_backlog
816 .fetch_add(1, Ordering::Relaxed);
817 continue;
818 } else {
819 let allowance = keyframe_allowance(
820 s.last_keyframe
821 .lock()
822 .unwrap()
823 .map(|(at, bytes)| (at.elapsed(), bytes)),
824 bitrate_kbps,
825 );
826 let over = backlog_exceeds(queued, threshold, allowance);
827 let sustained = backlog_sustained(
828 over,
829 &mut s.backlog_since.lock().unwrap(),
830 Instant::now(),
831 BACKLOG_SUSTAIN,
832 );
833 if wire_bytes > space || sustained {
834 self.inner
835 .counters
836 .frames_dropped_backlog
837 .fetch_add(1, Ordering::Relaxed);
838 s.skip_until_key.store(true, Ordering::Relaxed);
839 s.skip_deadline_ms.store(
840 self.now_ms() + self.profile().recovery_grace_ms as u64,
841 Ordering::Relaxed,
842 );
843 tracing::debug!(
844 frame_id,
845 wire_bytes,
846 queued,
847 allowance,
848 threshold,
849 "delta frame dropped: backlog; skipping until keyframe"
850 );
851 let asked = self.request_recovery(channel, "backlog");
852 self.arm_resume(asked);
853 continue;
854 }
855 }
856 let mut ok = true;
857 for c in &pack.chunks {
858 let mut tries = 0;
859 loop {
860 match s.transport.send_datagram(c.clone()) {
861 Ok(()) => break,
862 Err(SendError::Blocked) if frame.keyframe && tries < 50 => {
863 tries += 1;
864 tokio::time::sleep(Duration::from_millis(1)).await;
865 }
866 Err(_) => {
867 ok = false;
868 break;
869 }
870 }
871 }
872 if !ok {
873 break;
874 }
875 }
876 if ok {
877 sent_any = true;
878 if frame.keyframe {
879 *s.last_keyframe.lock().unwrap() = Some((Instant::now(), wire_bytes));
880 }
881 }
882 }
883 if sent_any {
884 let c = &self.inner.counters;
885 c.frames_sent.fetch_add(1, Ordering::Relaxed);
886 c.chunks_sent
887 .fetch_add(pack.chunks.len() as u64, Ordering::Relaxed);
888 c.parity_sent
889 .fetch_add(pack.parity_chunks as u64, Ordering::Relaxed);
890 c.bytes_sent.fetch_add(wire_bytes as u64, Ordering::Relaxed);
891 if frame.keyframe {
892 self.inner.abr.lock().unwrap().last_keyframe_sent = Some(Instant::now());
893 }
894 }
895 }
896
897 async fn run_session(self, transport: Arc<Session>) {
898 let tid = transport.session_id();
899 let remote = transport
900 .remote_addr()
901 .map(|a| a.to_string())
902 .unwrap_or_else(|| "relay".to_string());
903 let Ok(Some((mut reader, mut writer))) =
904 tokio::time::timeout(Duration::from_secs(5), transport.control()).await
905 else {
906 tracing::warn!(%remote, "session opened no control stream within 5 s");
907 transport.close(1, "no control stream");
908 return;
909 };
910 let Some(first) = seyd_transport::control::read_line(&mut reader).await else {
912 transport.close(1, "control stream closed");
913 return;
914 };
915 let (signal_id, _client) = match serde_json::from_str::<FromPilot>(&first) {
916 Ok(FromPilot::Hello {
917 proto: 2,
918 session_id,
919 client,
920 ..
921 }) => (session_id, client),
922 Ok(FromPilot::Hello { proto, .. }) => {
923 let _ = seyd_transport::control::write_line(
924 &mut writer,
925 &encode_line(&ToPilot::Denied {
926 reason: "unsupported proto",
927 }),
928 )
929 .await;
930 transport.close(1, &format!("proto {proto}"));
931 return;
932 }
933 _ => {
934 transport.close(1, "expected hello");
935 return;
936 }
937 };
938 let role = self
939 .inner
940 .pending
941 .lock()
942 .unwrap()
943 .remove(&signal_id)
944 .map(|p| p.role)
945 .unwrap_or(Role::Observer);
946 if self.session_count() >= self.inner.cfg.max_sessions {
947 let _ = seyd_transport::control::write_line(
948 &mut writer,
949 &encode_line(&ToPilot::Denied {
950 reason: "robot-busy",
951 }),
952 )
953 .await;
954 transport.close(1, "robot-busy");
955 return;
956 }
957
958 let (lines_tx, mut lines_rx) = mpsc::unbounded_channel::<String>();
959 let state = Arc::new(SessionState {
960 transport: transport.clone(),
961 signal_id: signal_id.clone(),
962 role,
963 lines: lines_tx,
964 skip_until_key: std::sync::atomic::AtomicBool::new(false),
965 skip_deadline_ms: std::sync::atomic::AtomicU64::new(0),
966 pilot: Mutex::new(PilotSample::default()),
967 last_keyframe: Mutex::new(None),
968 backlog_since: Mutex::new(None),
969 });
970 self.inner
971 .sessions
972 .lock()
973 .unwrap()
974 .insert(tid, state.clone());
975 let path_label = match transport.kind() {
978 seyd_transport::SessionKind::Relay => "relay",
979 seyd_transport::SessionKind::Direct => match transport.remote_addr() {
980 Some(a) if a.ip().is_ipv6() => "host6",
981 _ => "host",
982 },
983 }
984 .to_string();
985 let _ = state.lines.send(encode_line(&ToPilot::Welcome {
986 session_id: &signal_id,
987 role: role.as_str(),
988 channels: &self.inner.cfg.channels,
989 qos: self.profile().pilot_config(),
990 t_agent_us: self.now_us(),
991 }));
992 let _ = self
993 .inner
994 .events
995 .send(Event::SessionStarted {
996 signal_id: signal_id.clone(),
997 role,
998 path_label: path_label.clone(),
999 })
1000 .await;
1001 tracing::info!(session = %signal_id, %remote, role = role.as_str(), "session started");
1002
1003 let writer_task = tokio::spawn(async move {
1005 while let Some(line) = lines_rx.recv().await {
1006 if seyd_transport::control::write_line(&mut writer, &line)
1007 .await
1008 .is_err()
1009 {
1010 break;
1011 }
1012 }
1013 });
1014
1015 let engine = self.clone();
1017 let st = state.clone();
1018 let dg_task = tokio::spawn(async move {
1019 let Some(mut rx) = st.transport.take_datagrams() else {
1020 return;
1021 };
1022 while let Some(dg) = rx.recv().await {
1023 let Some((h, payload)) = ChunkHeader::parse(&dg) else {
1024 continue;
1025 };
1026 let Some(ch) = engine
1027 .inner
1028 .cfg
1029 .channels
1030 .iter()
1031 .find(|c| c.id == h.channel_id)
1032 else {
1033 continue;
1034 };
1035 if ch.kind != ChannelKind::Command || st.role != Role::Driver {
1036 continue;
1037 }
1038 let _ = engine
1039 .inner
1040 .events
1041 .send(Event::Command {
1042 channel: h.channel_id,
1043 payload: Bytes::copy_from_slice(payload),
1044 })
1045 .await;
1046 }
1047 });
1048
1049 let engine = self.clone();
1051 let st = state.clone();
1052 let stats_task = tokio::spawn(async move {
1053 let mut tick = tokio::time::interval(Duration::from_secs(1));
1054 loop {
1055 tick.tick().await;
1056 let c = &engine.inner.counters;
1057 let p = st.transport.stats();
1058 let abr = {
1059 let a = engine.inner.abr.lock().unwrap();
1060 AbrView {
1061 bitrate_kbps: a.bitrate_kbps,
1062 fec: a.fec,
1063 reason: a.reason,
1064 loss_pct: a.loss_pct,
1065 loss_pilot_pct: a.loss_pilot_pct,
1066 }
1067 };
1068 let v = serde_json::json!({
1069 "frames_in": c.frames_in.load(Ordering::Relaxed),
1070 "frames_sent": c.frames_sent.load(Ordering::Relaxed),
1071 "frames_dropped_backlog": c.frames_dropped_backlog.load(Ordering::Relaxed),
1072 "frames_skipped_stale": c.frames_skipped_stale.load(Ordering::Relaxed),
1073 "keyframes_requested": c.keyframes_requested.load(Ordering::Relaxed),
1074 "chunks_sent": c.chunks_sent.load(Ordering::Relaxed),
1075 "parity_sent": c.parity_sent.load(Ordering::Relaxed),
1076 "bytes_sent": c.bytes_sent.load(Ordering::Relaxed),
1077 "rtt_ms": p.rtt_ms, "min_rtt_ms": p.min_rtt_ms, "cwnd": p.cwnd,
1078 "delivery_kbps": p.delivery_kbps, "lost_packets": p.lost_packets, "mtu": p.current_mtu,
1079 "abr_bitrate_kbps": abr.bitrate_kbps, "abr_ceiling_kbps": engine.ceiling_kbps(0),
1080 "abr_fec_delta": abr.fec.0, "abr_fec_key": abr.fec.1, "abr_reason": abr.reason,
1081 "abr_loss_pct": abr.loss_pct.map(|x| (x * 10.0).round() / 10.0),
1082 "abr_loss_pilot_pct": abr.loss_pilot_pct.map(|x| (x * 10.0).round() / 10.0),
1083 });
1084 if st.lines.send(encode_line(&ToPilot::AgentStats(v))).is_err() {
1085 break;
1086 }
1087 }
1088 });
1089
1090 let mut reason = "pilot-closed".to_string();
1092 loop {
1093 tokio::select! {
1094 line = seyd_transport::control::read_line(&mut reader) => {
1095 let Some(line) = line else { break };
1096 match serde_json::from_str::<FromPilot>(&line) {
1097 Ok(FromPilot::Ping { t1 }) => {
1098 let _ = state.lines.send(encode_line(&ToPilot::Pong { t1, t2: self.now_us() }));
1099 }
1100 Ok(FromPilot::Loss { ch, .. }) => {
1101 let asked = self.request_recovery(ch, "pilot-loss");
1102 self.arm_resume(asked);
1103 }
1104 Ok(FromPilot::RequestKeyframe { ch }) => {
1105 let _ = self.request_recovery(ch, "pilot-request");
1107 }
1108 Ok(FromPilot::SetQos { profile }) => {
1109 match seyd_qos::get(&profile) {
1110 Some(p) => {
1111 self.set_profile(p, "pilot-request");
1112 let _ = state.lines.send(encode_line(&ToPilot::QosAck { profile: p.name, qos: p.pilot_config(), publisher: "requested" }));
1113 }
1114 None => tracing::debug!(%profile, "unknown qos profile"),
1115 }
1116 }
1117 Ok(FromPilot::PilotStats(v)) => {
1118 tracing::trace!(stats = %v, "pilot-stats");
1119 self.note_pilot_stats(&state, &v);
1120 }
1121 Ok(FromPilot::Bye) => { reason = "bye".into(); break; }
1122 Ok(FromPilot::Hello { .. }) => {}
1123 Ok(FromPilot::Unknown) => {}
1124 Err(e) => tracing::debug!(error = %e, "bad control line"),
1125 }
1126 }
1127 _ = transport.closed() => { reason = "transport-closed".into(); break; }
1128 }
1129 }
1130 self.inner.sessions.lock().unwrap().remove(&tid);
1131 transport.close(0, &reason);
1132 writer_task.abort();
1133 dg_task.abort();
1134 stats_task.abort();
1135 tracing::info!(session = %signal_id, %reason, "session ended");
1136 let _ = self
1137 .inner
1138 .events
1139 .send(Event::SessionEnded { signal_id, reason })
1140 .await;
1141 }
1142
1143 fn note_pilot_stats(&self, st: &SessionState, v: &serde_json::Value) {
1162 let get = |k: &str| v.get(k).and_then(|x| x.as_u64()).unwrap_or(0);
1163 let opt = |k: &str| v.get(k).and_then(|x| x.as_u64());
1164 let rx = get("chunks_rx");
1165 let missing = get("chunks_missing");
1166 let incomplete = get("frames_incomplete");
1167 let timed_out = get("frames_timed_out_late");
1169 let now = Instant::now();
1170 let pair = opt("chunks_sent_seen").zip(opt("chunks_rx_seen"));
1174 let mut ps = st.pilot.lock().unwrap();
1175 if let Some((sent_seen, rx_seen)) = pair {
1176 ps.pairs.push_back((now, sent_seen, rx_seen));
1177 while ps
1178 .pairs
1179 .front()
1180 .is_some_and(|(t, _, _)| now.duration_since(*t) > Duration::from_secs(5))
1181 {
1182 ps.pairs.pop_front();
1183 }
1184 if let (Some(first), Some(last)) = (ps.pairs.front(), ps.pairs.back()) {
1185 if let Some(pct) = windowed_loss_pct(
1186 last.0.duration_since(first.0),
1187 last.1.saturating_sub(first.1),
1188 last.2.saturating_sub(first.2),
1189 ) {
1190 ps.loss_pct = Some(pct);
1191 }
1192 }
1193 }
1194 if rx + missing > 0 {
1195 ps.loss_pilot_pct = Some(missing as f64 / (rx + missing) as f64 * 100.0);
1196 }
1197 if let Some((i0, t0)) = ps.last_incomplete {
1198 let genuine = incomplete
1199 .saturating_sub(i0)
1200 .saturating_sub(timed_out.saturating_sub(t0));
1201 ps.incomplete_delta += genuine;
1202 }
1203 ps.last_incomplete = Some((incomplete, timed_out));
1204 }
1205
1206 async fn abr_loop(self) {
1208 let mut tick = tokio::time::interval(Duration::from_secs(1));
1209 tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
1210 loop {
1211 tick.tick().await;
1212 let sessions: Vec<Arc<SessionState>> = self
1213 .inner
1214 .sessions
1215 .lock()
1216 .unwrap()
1217 .values()
1218 .cloned()
1219 .collect();
1220 if sessions.is_empty() {
1221 continue;
1222 }
1223 let profile = self.profile();
1224 let backlog_total = self
1225 .inner
1226 .counters
1227 .frames_dropped_backlog
1228 .load(Ordering::Relaxed);
1229 let mut sample = AbrSample::default();
1230 let mut worst_inflation = f64::MIN;
1231 let mut loss_pilot = 0.0f64;
1232 for st in &sessions {
1233 let p = st.transport.stats();
1234 let mut ps = st.pilot.lock().unwrap();
1235 let lost_delta = p.lost_packets.saturating_sub(ps.prev_lost_packets);
1236 ps.prev_lost_packets = p.lost_packets;
1237 let inflation = p.rtt_ms - p.min_rtt_ms;
1238 if inflation > worst_inflation {
1239 worst_inflation = inflation;
1240 sample.rtt_ms = p.rtt_ms;
1241 sample.min_rtt_ms = p.min_rtt_ms;
1242 }
1243 sample.delivery_kbps = sample.delivery_kbps.max(p.delivery_kbps);
1244 sample.send_kbps = sample.send_kbps.max(p.delivery_kbps);
1245 sample.lost_packets_delta += lost_delta;
1246 sample.pilot_frames_incomplete_delta += std::mem::take(&mut ps.incomplete_delta);
1247 if let Some(l) = ps.loss_pct {
1248 sample.pilot_chunk_loss_pct =
1249 Some(sample.pilot_chunk_loss_pct.map_or(l, |x: f64| x.max(l)));
1250 }
1251 loss_pilot = loss_pilot.max(ps.loss_pilot_pct.unwrap_or(0.0));
1252 }
1253 let mut abr = self.inner.abr.lock().unwrap();
1254 sample.keyframe_just_sent = abr
1255 .last_keyframe_sent
1256 .is_some_and(|t| t.elapsed() < Duration::from_millis(1200));
1257 abr.loss_pct = sample.pilot_chunk_loss_pct;
1258 abr.loss_pilot_pct = Some(loss_pilot);
1259 sample.frames_dropped_backlog_delta = backlog_total.saturating_sub(abr.prev_backlog);
1260 abr.prev_backlog = backlog_total;
1261 let video_channels: Vec<u8> = self
1262 .inner
1263 .cfg
1264 .channels
1265 .iter()
1266 .filter(|c| c.kind == ChannelKind::Video)
1267 .map(|c| c.id)
1268 .collect();
1269 for ch in video_channels.iter().copied() {
1270 let ctl = abr.controllers.entry(ch).or_insert_with(|| {
1271 AbrController::with_ceiling(profile, self.inner.publisher_max_kbps(ch))
1272 });
1273 let Some(d) = ctl.step(&sample) else { continue };
1274 abr.fec = (d.fec_delta_pct, d.fec_key_pct);
1275 abr.reason = d.reason;
1276 if d.bitrate_changed {
1277 let up = d.max_bitrate_kbps > abr.bitrate_kbps;
1278 abr.bitrate_kbps = d.max_bitrate_kbps;
1279 let mut cfg = self.inner.publisher_config(
1280 profile,
1281 ch,
1282 if up { "abr-up" } else { "abr-down" },
1283 );
1284 cfg["maxBitrateKbps"] = serde_json::json!(d.max_bitrate_kbps);
1285 cfg["suggestedFps"] = serde_json::json!(d.suggested_fps);
1286 let _ = self.inner.events.try_send(Event::RequestedConfig(cfg));
1287 }
1288 tracing::info!(
1289 channel = ch,
1290 bitrate = d.max_bitrate_kbps,
1291 fec_delta = d.fec_delta_pct,
1292 fec_key = d.fec_key_pct,
1293 reason = d.reason,
1294 loss = ?sample.pilot_chunk_loss_pct,
1295 loss_pilot_est = loss_pilot,
1296 residual = sample.pilot_frames_incomplete_delta,
1297 kf_recent = sample.keyframe_just_sent,
1298 rtt = sample.rtt_ms,
1299 min_rtt = sample.min_rtt_ms,
1300 "abr"
1301 );
1302 }
1303
1304 let target = abr.bitrate_kbps;
1308 let mut sim = self.inner.simulcast.lock().unwrap();
1309 for ch in video_channels {
1310 let Some(st) = sim.get_mut(&ch) else { continue };
1311 let Some(sel) = st.selector.step(target) else {
1312 continue;
1313 };
1314 st.pending = Some(sel.layer);
1317 let _ = self.inner.events.try_send(Event::LayerChanged {
1318 channel: ch,
1319 layer: sel.layer,
1320 name: st.selector.current_name().to_string(),
1321 reason: sel.reason,
1322 });
1323 tracing::info!(
1324 channel = ch,
1325 layer = sel.layer,
1326 name = st.selector.current_name(),
1327 reason = sel.reason,
1328 target_kbps = target,
1329 "simulcast"
1330 );
1331 }
1332 }
1333 }
1334
1335 fn now_ms(&self) -> u64 {
1336 self.inner.epoch.elapsed().as_millis() as u64
1337 }
1338
1339 fn arm_resume(&self, asked: Option<Recovery>) {
1346 let Some(kind) = asked else { return };
1347 if kind.yields_keyframe() {
1348 return;
1349 }
1350 let grace = Duration::from_millis(self.profile().recovery_grace_ms as u64);
1351 let mut q = self.inner.queue.lock().unwrap();
1352 q.resume_at = Some(Instant::now() + grace);
1353 }
1354
1355 fn request_recovery(&self, channel: u8, reason: &'static str) -> Option<Recovery> {
1361 let min_gap = Duration::from_millis(250);
1366 let now = Instant::now();
1367 let mut last = self.inner.last_recovery.lock().unwrap();
1368 let previous = last.get(&channel).copied();
1369 if let Some(p) = previous {
1370 if now.duration_since(p.last) < min_gap {
1371 return None;
1372 }
1373 }
1374
1375 let grace = Duration::from_millis(self.profile().recovery_grace_ms as u64);
1376 let kind = if reason == "pilot-request" {
1377 Recovery::Idr
1380 } else if !self.inner.cfg.recovery_ladder {
1381 Recovery::Idr
1382 } else {
1383 match previous {
1384 Some(p) if now.duration_since(p.last) >= RECOVERY_ESCALATE_WINDOW => Recovery::Ltr,
1386 Some(p) if now.duration_since(p.rung_since) >= grace => p.rung.escalate(),
1389 Some(p) => p.rung,
1391 None => Recovery::Ltr,
1392 }
1393 };
1394
1395 let ladder_rung = reason != "pilot-request";
1400 let (rung, rung_since) = match previous {
1401 _ if !ladder_rung => previous
1402 .map(|p| (p.rung, p.rung_since))
1403 .unwrap_or((Recovery::Ltr, now)),
1404 Some(p) if p.rung == kind => (kind, p.rung_since),
1405 _ => (kind, now),
1406 };
1407 last.insert(
1408 channel,
1409 RecoveryState {
1410 last: now,
1411 rung,
1412 rung_since,
1413 },
1414 );
1415 drop(last);
1416
1417 self.inner
1418 .counters
1419 .keyframes_requested
1420 .fetch_add(1, Ordering::Relaxed);
1421 let _ = self.inner.events.try_send(Event::RecoveryRequest {
1422 channel,
1423 kind: kind.as_str(),
1424 reason,
1425 });
1426 Some(kind)
1427 }
1428}
1429
1430#[cfg(test)]
1431mod tests {
1432 use super::*;
1433 use seyd_qos::BALANCED;
1434
1435 const W: Duration = Duration::from_secs(5);
1436
1437 #[test]
1438 fn clean_link_reads_zero() {
1439 assert_eq!(windowed_loss_pct(W, 1000, 1000), Some(0.0));
1440 }
1441
1442 fn near(got: Option<f64>, want: f64) {
1443 let g = got.expect("expected a measurement");
1444 assert!((g - want).abs() < 1e-9, "got {g}, want {want}");
1445 }
1446
1447 #[test]
1448 fn loss_is_the_shortfall_against_what_was_sent() {
1449 near(windowed_loss_pct(W, 1000, 950), 5.0);
1450 near(windowed_loss_pct(W, 200, 100), 50.0);
1451 }
1452
1453 #[test]
1454 fn in_flight_skew_reads_zero_not_negative() {
1455 assert_eq!(windowed_loss_pct(W, 1000, 1008), Some(0.0));
1458 }
1459
1460 #[test]
1461 fn a_window_too_short_or_too_small_yields_nothing() {
1462 assert_eq!(windowed_loss_pct(Duration::from_secs(2), 1000, 900), None);
1463 assert_eq!(windowed_loss_pct(W, 49, 0), None);
1464 }
1465
1466 #[test]
1467 fn a_keyframe_keeps_a_shallow_contiguous_queue() {
1468 assert!(!keyframe_flushes_queue(0, false));
1472 assert!(!keyframe_flushes_queue(1, false));
1473 assert!(!keyframe_flushes_queue(KEYFRAME_FLUSH_FRAMES - 1, false));
1474 }
1475
1476 #[test]
1477 fn a_keyframe_flushes_deltas_that_lead_nowhere() {
1478 assert!(keyframe_flushes_queue(1, true));
1481 assert!(keyframe_flushes_queue(0, true));
1482 assert!(keyframe_flushes_queue(KEYFRAME_FLUSH_FRAMES, false));
1484 assert!(keyframe_flushes_queue(MAX_QUEUED_FRAMES, false));
1485 }
1486
1487 #[test]
1488 fn backlog_is_measured_on_queued_bytes_not_free_space() {
1489 let threshold = BALANCED.drop_threshold_bytes(30);
1491 assert!(!backlog_exceeds(0, threshold, 0));
1492 assert!(
1493 !backlog_exceeds(threshold, threshold, 0),
1494 "at the budget still admits"
1495 );
1496 assert!(backlog_exceeds(threshold + 1, threshold, 0));
1499 }
1500
1501 #[test]
1502 fn a_backlog_shorter_than_a_probe_rtt_does_not_drop() {
1503 let t0 = Instant::now();
1507 let mut since = None;
1508 assert!(!backlog_sustained(true, &mut since, t0, BACKLOG_SUSTAIN));
1509 assert!(!backlog_sustained(
1510 true,
1511 &mut since,
1512 t0 + Duration::from_millis(200),
1513 BACKLOG_SUSTAIN
1514 ));
1515 assert!(!backlog_sustained(
1517 false,
1518 &mut since,
1519 t0 + Duration::from_millis(250),
1520 BACKLOG_SUSTAIN
1521 ));
1522 assert!(since.is_none());
1523 let t1 = t0 + Duration::from_secs(1);
1525 assert!(!backlog_sustained(true, &mut since, t1, BACKLOG_SUSTAIN));
1526 assert!(backlog_sustained(
1527 true,
1528 &mut since,
1529 t1 + BACKLOG_SUSTAIN,
1530 BACKLOG_SUSTAIN
1531 ));
1532 assert!(backlog_sustained(
1533 true,
1534 &mut since,
1535 t1 + Duration::from_secs(2),
1536 BACKLOG_SUSTAIN
1537 ));
1538 }
1539
1540 #[test]
1541 fn a_draining_keyframe_is_not_backlog() {
1542 let threshold = BALANCED.drop_threshold_bytes(30);
1543 let kf = 30_000; let a = keyframe_allowance(Some((Duration::from_millis(30), kf)), 3000);
1547 assert_eq!(a, kf);
1548 assert!(!backlog_exceeds(kf + 12_500, threshold, a));
1549 assert!(backlog_exceeds(kf + threshold + 1, threshold, a));
1551 let late = keyframe_allowance(Some((Duration::from_millis(500), kf)), 3000);
1553 assert_eq!(late, 0);
1554 assert!(backlog_exceeds(kf + 12_500, threshold, late));
1555 assert_eq!(keyframe_allowance(None, 3000), 0);
1557 }
1558
1559 #[test]
1560 fn a_slower_bitrate_request_gives_the_keyframe_longer_to_drain() {
1561 let kf = 60_000; assert_eq!(
1563 keyframe_allowance(Some((Duration::from_millis(600), kf)), 1500),
1564 kf
1565 );
1566 assert_eq!(
1567 keyframe_allowance(Some((Duration::from_millis(700), kf)), 1500),
1568 0
1569 );
1570 }
1571
1572 #[test]
1573 fn keeping_a_queue_still_leaves_the_sender_room() {
1574 let kept = KEYFRAME_FLUSH_FRAMES - 1;
1577 assert!(kept + 1 < MAX_QUEUED_FRAMES);
1578 }
1579
1580 #[test]
1588 fn a_changing_send_rate_is_not_loss() {
1589 let mut sent = 0u64;
1590 let mut rx = 0u64;
1591 let mut pairs = Vec::new();
1593 for i in 0..6u64 {
1594 pairs.push((sent, rx));
1595 let rate = 60 + i * 16;
1596 sent += rate;
1597 rx += rate; }
1599 let (s0, r0) = pairs[0];
1600 let (s1, r1) = *pairs.last().unwrap();
1601 assert_eq!(windowed_loss_pct(W, s1 - s0, r1 - r0), Some(0.0));
1602 }
1603}
1604
1605#[cfg(test)]
1606mod recovery_tests {
1607 use super::*;
1608
1609 #[test]
1610 fn the_ladder_climbs_on_repeat_and_resets_when_quiet() {
1611 assert_eq!(Recovery::Ltr.escalate(), Recovery::IntraRefresh);
1612 assert_eq!(Recovery::IntraRefresh.escalate(), Recovery::Idr);
1613 assert_eq!(Recovery::Idr.escalate(), Recovery::Idr);
1615 }
1616
1617 #[test]
1618 fn only_an_idr_promises_a_keyframe() {
1619 assert!(Recovery::Idr.yields_keyframe());
1622 assert!(!Recovery::Ltr.yields_keyframe());
1623 assert!(!Recovery::IntraRefresh.yields_keyframe());
1624 }
1625
1626 #[test]
1627 fn the_wire_names_match_the_documented_publisher_control_vocabulary() {
1628 assert_eq!(Recovery::Ltr.as_str(), "ltr");
1631 assert_eq!(Recovery::IntraRefresh.as_str(), "intra_refresh");
1632 assert_eq!(Recovery::Idr.as_str(), "idr");
1633 }
1634
1635 #[test]
1636 fn a_queue_waiting_on_a_keyframe_releases_itself_when_the_grace_expires() {
1637 let mut q = FrameQueue {
1641 frames: VecDeque::new(),
1642 skip_until_key: true,
1643 resume_at: Some(Instant::now() - Duration::from_millis(1)),
1644 };
1645 assert!(q.resume_due(), "an expired grace must release the latch");
1646
1647 q.resume_at = Some(Instant::now() + Duration::from_secs(5));
1648 assert!(!q.resume_due(), "an unexpired grace must still hold it");
1649
1650 q.resume_at = None;
1652 assert!(!q.resume_due());
1653 }
1654}