Skip to main content

oxpulse_sfu_kit/client/
test_seed.rs

1//! Test-only seam that builds a `Client` without real str0m SDP negotiation.
2//!
3//! Used by integration tests to verify fanout semantics in isolation without
4//! spinning up a full ICE/DTLS pipeline.
5
6use std::sync::Arc;
7use std::time::Instant;
8
9use str0m::format::{Codec, CodecExtra, CodecSpec, FormatParams, PayloadParams};
10use str0m::media::{Frequency, MediaData, MediaKind, MediaTime, Mid, Pt, Rid};
11use str0m::rtp::{ExtensionValues, SeqNo};
12
13use crate::ids::SfuRid;
14use crate::media::SfuMediaPayload;
15use crate::rtc::SfuRtc;
16
17use super::tracks::{TrackIn, TrackInEntry};
18use super::Client;
19use crate::metrics::SfuMetrics;
20use crate::propagate::ClientId;
21
22impl Client {
23    /// Inject an observed publisher RID without running the `track_in_media` path.
24    ///
25    /// Production code should never call this — `track_in_media` owns the
26    /// canonical write. Used by screenshare-like tests that need to pin
27    /// `active_rids` to a subset of the full simulcast ladder.
28    pub fn seed_active_rid_for_tests(&mut self, rid: Rid) {
29        self.active_rids.insert(SfuRid::from_str0m(rid));
30    }
31
32    /// Mark the underlying `Rtc` as disconnected so `is_alive` returns false.
33    ///
34    /// Needed for `reap_dead` tests — the real disconnect path requires an
35    /// ICE/DTLS pipeline that integration tests don't set up.
36    pub fn disconnect_for_tests(&mut self) {
37        self.rtc.disconnect();
38    }
39}
40
41/// Build a `Client` wrapping a default `Rtc` with the given `ClientId`.
42///
43/// The `Rtc` is unnegotiated — writer calls inside `handle_media_data_out`
44/// will no-op, but the `delivered_media` counter still ticks so fanout is
45/// observable from tests.
46pub fn new_client(id: ClientId) -> Client {
47    let rtc = SfuRtc::from_raw(str0m::Rtc::builder().build(Instant::now()));
48    let metrics = Arc::new(SfuMetrics::new_default());
49    let mut c = Client::new(rtc, metrics);
50    c.id = id;
51    c
52}
53
54/// Seed an incoming track on `client`.
55///
56/// Returns the `Arc<TrackIn>` so the caller can `Arc::downgrade` it into
57/// other clients' `tracks_out`.
58pub fn seed_track_in(client: &mut Client, mid_tag: u8, kind: MediaKind) -> Arc<TrackIn> {
59    let mid: Mid = Mid::from(&*format!("m{mid_tag}"));
60    let entry = TrackInEntry {
61        id: Arc::new(TrackIn {
62            origin: client.id,
63            mid,
64            kind,
65            relay_source: false,
66        }),
67        last_keyframe_request: None,
68    };
69    let arc = entry.id.clone();
70    client.tracks_in.push(entry);
71    arc
72}
73
74/// Build a synthetic `SfuMediaPayload` for the given mid tag and optional RID.
75///
76/// Used by fanout / simulcast filter tests to inject packets without running
77/// RTP packetization. The layer filter runs before any writer call, so
78/// tests observe filter semantics purely via the `delivered_media` counter.
79pub fn make_media_data(mid_tag: u8, rid: Option<SfuRid>) -> SfuMediaPayload {
80    let mid: Mid = Mid::from(&*format!("m{mid_tag}"));
81    let pt = Pt::from(96u8);
82    let seq: SeqNo = 0u64.into();
83    let params = PayloadParams::new(
84        pt,
85        None,
86        CodecSpec {
87            codec: Codec::Vp8,
88            clock_rate: Frequency::NINETY_KHZ,
89            channels: None,
90            format: FormatParams::default(),
91        },
92    );
93    let raw = MediaData {
94        mid,
95        pt,
96        rid: rid.map(|r| r.to_str0m()),
97        params,
98        time: MediaTime::from_90khz(0),
99        network_time: Instant::now(),
100        seq_range: seq..=seq,
101        data: vec![0xde, 0xad, 0xbe, 0xef],
102        ext_vals: ExtensionValues::default(),
103        codec_extra: CodecExtra::None,
104        contiguous: true,
105        last_sender_info: None,
106        audio_start_of_talk_spurt: false,
107    };
108    SfuMediaPayload::from_str0m(raw)
109}
110
111/// Seed an incoming track on `client` as if the client were a relay source.
112///
113/// Identical to [`seed_track_in`] except `relay_source = true` — so the
114/// keyframe-routing path treats this track as originating from an upstream SFU.
115pub fn seed_track_in_relay(client: &mut Client, mid_tag: u8, kind: MediaKind) -> Arc<TrackIn> {
116    let mid: Mid = Mid::from(&*format!("m{mid_tag}"));
117    let entry = TrackInEntry {
118        id: Arc::new(TrackIn {
119            origin: client.id,
120            mid,
121            kind,
122            relay_source: true,
123        }),
124        last_keyframe_request: None,
125    };
126    let arc = entry.id.clone();
127    client.tracks_in.push(entry);
128    arc
129}
130
131/// Force a track-out entry into `Open` state so `incoming_keyframe_req` can
132/// find it by MID.
133///
134/// In production this state is set during SDP negotiation. In tests we skip
135/// that pipeline and pin the MID directly so keyframe routing can be exercised.
136pub fn open_track_out_for_tests(subscriber: &mut Client, track_in: &Arc<TrackIn>) {
137    for track_out in subscriber.tracks_out.iter_mut() {
138        if track_out.track_in.upgrade().as_deref().map(|t| t.mid) == Some(track_in.mid) {
139            track_out.state = crate::client::tracks::TrackOutState::Open(track_in.mid);
140            return;
141        }
142    }
143    panic!("no track_out found for mid {:?}", track_in.mid);
144}