Skip to main content

openipc_core/
receiver.rs

1use crate::channel::ChannelId;
2use crate::ieee80211::FrameLayout;
3use crate::realtek::{parse_rx_aggregate, AggregateError, RealtekRxPacket, RxPacketType};
4use crate::routes::{
5    PayloadRouteError, PayloadRouteEvent, PayloadRouteId, PayloadRouteManager, PayloadRuntimeKey,
6};
7use crate::rtp::{DepacketizedFrame, RtpDepacketizer, RtpHeader};
8use crate::wfb::{FecCounters, WfbKeypair};
9
10/// Shared receive runtime for OpenIPC video plus optional raw payload taps.
11///
12/// This is the easiest core entry point for apps. It accepts Realtek RX
13/// transfers, 802.11 frames, or already-decrypted fragments; routes recovered
14/// WFB payloads by route id; and depacketizes the configured video route from
15/// RTP into Annex-B H.264/H.265 frames.
16#[derive(Debug, Clone)]
17pub struct ReceiverRuntime {
18    routes: PayloadRouteManager,
19    video_runtime: PayloadRuntimeKey,
20    video_route_id: PayloadRouteId,
21    rtp: RtpDepacketizer,
22}
23
24/// Options that control how one receive batch is processed.
25#[derive(Debug, Clone, Default)]
26pub struct ReceiverBatchOptions {
27    /// Keep CRC/ICV-marked packets instead of dropping them before WFB parsing.
28    pub accept_corrupted: bool,
29    /// Route ids whose recovered payload bytes should be copied into the batch.
30    pub raw_payload_routes: Vec<PayloadRouteId>,
31    /// RTP payload-type filters whose matching packets should be copied.
32    pub rtp_payload_taps: Vec<RtpPayloadTap>,
33}
34
35/// Filter for copying RTP packets from a recovered route.
36///
37/// This is useful for mixed media routes. For example, OpenIPC can carry Opus
38/// audio as RTP payload type 98 on the same WFB route as video. A tap lets an
39/// app copy only those audio RTP packets while the built-in video depacketizer
40/// continues to consume the same route for H.264/H.265.
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub struct RtpPayloadTap {
43    /// Application route id whose recovered payloads should be inspected.
44    pub route_id: PayloadRouteId,
45    /// RTP payload type to copy.
46    pub payload_type: u8,
47}
48
49/// Recovered WFB payload bytes copied for an application route.
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct RoutePayload {
52    /// Application-defined route id that requested this payload tap.
53    pub route_id: PayloadRouteId,
54    /// WFB channel id that carried the payload.
55    pub channel_id: ChannelId,
56    /// Recovered WFB packet sequence number.
57    pub packet_seq: u64,
58    /// Raw recovered payload bytes.
59    pub data: Vec<u8>,
60}
61
62/// Counters collected while processing one receive batch.
63#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
64pub struct ReceiverBatchCounters {
65    /// Realtek RX packets seen in the batch.
66    pub packets: usize,
67    /// Packets accepted after Realtek descriptor filtering.
68    pub accepted_packets: usize,
69    /// Packets dropped by descriptor filtering.
70    pub dropped_packets: usize,
71    /// Packets dropped because the Realtek descriptor reported a CRC error.
72    pub crc_dropped: usize,
73    /// Packets dropped because the Realtek descriptor reported an ICV error.
74    pub icv_dropped: usize,
75    /// Packets dropped because they were not normal RX packets.
76    pub report_dropped: usize,
77    /// 802.11 frames that did not match any configured route or payload shape.
78    pub ignored_frames: usize,
79    /// WFB session packets accepted by configured routes.
80    pub sessions: usize,
81    /// Recovered payloads on the configured video route.
82    pub wfb_payloads: usize,
83    /// RTP packets observed on the configured video route.
84    pub rtp_packets: usize,
85    /// Annex-B frames emitted by the RTP depacketizer.
86    pub video_frames: usize,
87    /// Raw payload copies emitted for routes in [`ReceiverBatchOptions`].
88    pub raw_payload_count: usize,
89    /// Total bytes copied into raw payload outputs.
90    pub raw_payload_bytes: usize,
91    /// Route-manager errors treated as dropped/ignored frames.
92    pub route_errors: usize,
93}
94
95/// Output produced from one transfer, packet list, frame, or fragment push.
96#[derive(Debug, Clone, PartialEq, Eq)]
97pub struct ReceiverBatch {
98    /// Encoded Annex-B video frames from the configured video route.
99    pub frames: Vec<DepacketizedFrame>,
100    /// Raw payload bytes for requested route taps.
101    pub raw_payloads: Vec<RoutePayload>,
102    /// Per-batch parser and routing counters.
103    pub counters: ReceiverBatchCounters,
104    /// Current cumulative FEC counters for the video runtime.
105    pub fec_counters: FecCounters,
106}
107
108impl ReceiverRuntime {
109    /// Build a runtime around an existing route manager.
110    ///
111    /// `video_runtime` and `video_route_id` identify the route whose recovered
112    /// payloads are RTP video and should be depacketized into frames.
113    pub fn from_routes(
114        routes: PayloadRouteManager,
115        video_runtime: PayloadRuntimeKey,
116        video_route_id: PayloadRouteId,
117    ) -> Self {
118        Self {
119            routes,
120            video_runtime,
121            video_route_id,
122            rtp: RtpDepacketizer::new(),
123        }
124    }
125
126    /// Create a runtime with an unencrypted/plain video route.
127    ///
128    /// This is mainly useful for tests and pre-decrypted captures.
129    pub fn with_plain_video_route(
130        frame_layout: FrameLayout,
131        video_route_id: PayloadRouteId,
132        channel_id: ChannelId,
133        key_slot: u64,
134        fec_k: usize,
135        fec_n: usize,
136    ) -> Result<Self, PayloadRouteError> {
137        let mut routes = PayloadRouteManager::new(frame_layout);
138        let video_runtime =
139            routes.add_plain_route(video_route_id, channel_id, key_slot, fec_k, fec_n)?;
140        Ok(Self::from_routes(routes, video_runtime, video_route_id))
141    }
142
143    /// Create a runtime with an encrypted WFB video route.
144    pub fn with_keyed_video_route(
145        frame_layout: FrameLayout,
146        video_route_id: PayloadRouteId,
147        channel_id: ChannelId,
148        key_slot: u64,
149        keypair: WfbKeypair,
150        minimum_epoch: u64,
151    ) -> Result<Self, PayloadRouteError> {
152        let mut routes = PayloadRouteManager::new(frame_layout);
153        let video_runtime =
154            routes.add_keyed_route(video_route_id, channel_id, key_slot, keypair, minimum_epoch)?;
155        Ok(Self::from_routes(routes, video_runtime, video_route_id))
156    }
157
158    /// Return the route-manager runtime key used for video.
159    pub const fn video_runtime(&self) -> PayloadRuntimeKey {
160        self.video_runtime
161    }
162
163    /// Return the application route id used for video.
164    pub const fn video_route_id(&self) -> PayloadRouteId {
165        self.video_route_id
166    }
167
168    /// Borrow the underlying route manager.
169    pub fn routes(&self) -> &PayloadRouteManager {
170        &self.routes
171    }
172
173    /// Mutably borrow the underlying route manager.
174    pub fn routes_mut(&mut self) -> &mut PayloadRouteManager {
175        &mut self.routes
176    }
177
178    /// Mutably borrow the RTP depacketizer for advanced video handling.
179    pub fn rtp_mut(&mut self) -> &mut RtpDepacketizer {
180        &mut self.rtp
181    }
182
183    /// Add an unencrypted/plain raw-payload route.
184    pub fn add_plain_route(
185        &mut self,
186        route_id: PayloadRouteId,
187        channel_id: ChannelId,
188        key_slot: u64,
189        fec_k: usize,
190        fec_n: usize,
191    ) -> Result<PayloadRuntimeKey, PayloadRouteError> {
192        self.routes
193            .add_plain_route(route_id, channel_id, key_slot, fec_k, fec_n)
194    }
195
196    /// Add an encrypted WFB raw-payload route.
197    pub fn add_keyed_route(
198        &mut self,
199        route_id: PayloadRouteId,
200        channel_id: ChannelId,
201        key_slot: u64,
202        keypair: WfbKeypair,
203        minimum_epoch: u64,
204    ) -> Result<PayloadRuntimeKey, PayloadRouteError> {
205        self.routes
206            .add_keyed_route(route_id, channel_id, key_slot, keypair, minimum_epoch)
207    }
208
209    /// Return cumulative FEC counters for the video runtime.
210    pub fn video_fec_counters(&self) -> FecCounters {
211        self.routes
212            .fec_counters(self.video_runtime)
213            .unwrap_or_default()
214    }
215
216    /// Return true if an 802.11 frame belongs to the configured video runtime.
217    pub fn accepts_video_frame(&self, frame: &[u8]) -> bool {
218        self.routes.accepts_80211_frame(self.video_runtime, frame)
219    }
220
221    /// Parse and process one Realtek USB RX transfer.
222    pub fn push_rx_transfer(
223        &mut self,
224        transfer: &[u8],
225        options: &ReceiverBatchOptions,
226    ) -> Result<ReceiverBatch, AggregateError> {
227        let packets = parse_rx_aggregate(transfer)?;
228        Ok(self.push_rx_packets(packets, options))
229    }
230
231    /// Process already parsed Realtek RX packets.
232    pub fn push_rx_packets<'a>(
233        &mut self,
234        packets: impl IntoIterator<Item = RealtekRxPacket<'a>>,
235        options: &ReceiverBatchOptions,
236    ) -> ReceiverBatch {
237        let mut batch = self.empty_batch();
238
239        for packet in packets {
240            batch.counters.packets += 1;
241            if packet.attrib.crc_err && !options.accept_corrupted {
242                batch.counters.crc_dropped += 1;
243                continue;
244            }
245            if packet.attrib.icv_err && !options.accept_corrupted {
246                batch.counters.icv_dropped += 1;
247                continue;
248            }
249            if packet.attrib.pkt_rpt_type != RxPacketType::NormalRx {
250                batch.counters.report_dropped += 1;
251                continue;
252            }
253
254            batch.counters.accepted_packets += 1;
255            match self.routes.push_80211_frame(packet.data) {
256                Ok(events) => self.apply_route_events(events, options, &mut batch),
257                Err(_) => {
258                    batch.counters.ignored_frames += 1;
259                    batch.counters.route_errors += 1;
260                }
261            }
262        }
263
264        batch.counters.dropped_packets =
265            batch.counters.crc_dropped + batch.counters.icv_dropped + batch.counters.report_dropped;
266        batch.fec_counters = self.video_fec_counters();
267        batch
268    }
269
270    /// Process one OpenIPC/WFB 802.11 frame.
271    pub fn push_80211_frame(
272        &mut self,
273        frame: &[u8],
274        options: &ReceiverBatchOptions,
275    ) -> Result<ReceiverBatch, PayloadRouteError> {
276        let mut batch = self.empty_batch();
277        let events = self.routes.push_80211_frame(frame)?;
278        self.apply_route_events(events, options, &mut batch);
279        batch.fec_counters = self.video_fec_counters();
280        Ok(batch)
281    }
282
283    /// Process one 802.11 frame when the caller already decrypted the WFB fragment.
284    pub fn push_decrypted_80211_frame(
285        &mut self,
286        runtime: PayloadRuntimeKey,
287        frame: &[u8],
288        decrypted_fragment: &[u8],
289        options: &ReceiverBatchOptions,
290    ) -> Result<ReceiverBatch, PayloadRouteError> {
291        let mut batch = self.empty_batch();
292        let events = self
293            .routes
294            .push_decrypted_80211_frame(runtime, frame, decrypted_fragment)?;
295        self.apply_route_events(events, options, &mut batch);
296        batch.fec_counters = self.video_fec_counters();
297        Ok(batch)
298    }
299
300    /// Process one already-decrypted WFB fragment.
301    pub fn push_decrypted_fragment(
302        &mut self,
303        runtime: PayloadRuntimeKey,
304        data_nonce: u64,
305        decrypted_fragment: &[u8],
306        options: &ReceiverBatchOptions,
307    ) -> Result<ReceiverBatch, PayloadRouteError> {
308        let mut batch = self.empty_batch();
309        let events =
310            self.routes
311                .push_decrypted_fragment(runtime, data_nonce, decrypted_fragment)?;
312        self.apply_route_events(events, options, &mut batch);
313        batch.fec_counters = self.video_fec_counters();
314        Ok(batch)
315    }
316
317    fn empty_batch(&self) -> ReceiverBatch {
318        ReceiverBatch {
319            frames: Vec::new(),
320            raw_payloads: Vec::new(),
321            counters: ReceiverBatchCounters::default(),
322            fec_counters: self.video_fec_counters(),
323        }
324    }
325
326    fn apply_route_events(
327        &mut self,
328        events: Vec<PayloadRouteEvent>,
329        options: &ReceiverBatchOptions,
330        batch: &mut ReceiverBatch,
331    ) {
332        for event in events {
333            match event {
334                PayloadRouteEvent::IgnoredFrame => batch.counters.ignored_frames += 1,
335                PayloadRouteEvent::SessionEstablished { .. } => batch.counters.sessions += 1,
336                PayloadRouteEvent::Payload {
337                    route_ids, payload, ..
338                } => {
339                    if route_ids.contains(&self.video_route_id) {
340                        batch.counters.wfb_payloads += 1;
341                        batch.counters.rtp_packets += 1;
342                        if let Ok(Some(frame)) = self.rtp.push(&payload.data) {
343                            batch.counters.video_frames += 1;
344                            batch.frames.push(frame);
345                        }
346                    }
347
348                    for &route_id in &route_ids {
349                        if !options.raw_payload_routes.contains(&route_id) {
350                            continue;
351                        }
352                        copy_raw_payload(route_id, &payload, batch);
353                    }
354
355                    if options.rtp_payload_taps.is_empty() {
356                        continue;
357                    }
358                    let Ok(header) = RtpHeader::parse(&payload.data) else {
359                        continue;
360                    };
361                    for tap in &options.rtp_payload_taps {
362                        if header.payload_type != tap.payload_type {
363                            continue;
364                        }
365                        if !route_ids.contains(&tap.route_id) {
366                            continue;
367                        }
368                        copy_raw_payload(tap.route_id, &payload, batch);
369                    }
370                }
371            }
372        }
373    }
374}
375
376fn copy_raw_payload(
377    route_id: PayloadRouteId,
378    payload: &crate::pipeline::RecoveredPayload,
379    batch: &mut ReceiverBatch,
380) {
381    batch.counters.raw_payload_count += 1;
382    batch.counters.raw_payload_bytes += payload.data.len();
383    batch.raw_payloads.push(RoutePayload {
384        route_id,
385        channel_id: payload.channel_id,
386        packet_seq: payload.packet_seq,
387        data: payload.data.clone(),
388    });
389}
390
391#[cfg(test)]
392mod tests {
393    use super::*;
394    use crate::RadioPort;
395
396    fn plain(payload: &[u8]) -> Vec<u8> {
397        let mut out = Vec::new();
398        out.push(0);
399        out.extend_from_slice(&(payload.len() as u16).to_be_bytes());
400        out.extend_from_slice(payload);
401        out
402    }
403
404    fn rtp(payload_type: u8, payload: &[u8]) -> Vec<u8> {
405        let mut packet = vec![0x80, payload_type & 0x7f];
406        packet.extend_from_slice(&7u16.to_be_bytes());
407        packet.extend_from_slice(&48_000u32.to_be_bytes());
408        packet.extend_from_slice(&0x1122_3344u32.to_be_bytes());
409        packet.extend_from_slice(payload);
410        packet
411    }
412
413    #[test]
414    fn decrypted_fragment_can_fan_out_to_raw_route() {
415        let route = PayloadRouteId::new(7);
416        let mut runtime = ReceiverRuntime::with_plain_video_route(
417            FrameLayout::WithFcs,
418            route,
419            ChannelId::default_video(),
420            0,
421            1,
422            1,
423        )
424        .unwrap();
425        let batch = runtime
426            .push_decrypted_fragment(
427                runtime.video_runtime(),
428                0,
429                &plain(b"payload bytes"),
430                &ReceiverBatchOptions {
431                    raw_payload_routes: vec![route],
432                    ..ReceiverBatchOptions::default()
433                },
434            )
435            .unwrap();
436
437        assert_eq!(batch.counters.wfb_payloads, 1);
438        assert_eq!(batch.counters.raw_payload_count, 1);
439        assert_eq!(batch.raw_payloads[0].data, b"payload bytes");
440    }
441
442    #[test]
443    fn rtp_payload_tap_copies_only_matching_payload_type() {
444        let video_route = PayloadRouteId::new(1);
445        let audio_route = PayloadRouteId::new(3);
446        let mut runtime = ReceiverRuntime::with_plain_video_route(
447            FrameLayout::WithFcs,
448            video_route,
449            ChannelId::default_video(),
450            0,
451            1,
452            1,
453        )
454        .unwrap();
455        runtime
456            .add_plain_route(audio_route, ChannelId::default_video(), 0, 1, 1)
457            .unwrap();
458
459        let ignored = runtime
460            .push_decrypted_fragment(
461                runtime.video_runtime(),
462                0,
463                &plain(&rtp(crate::rtp::RTP_PAYLOAD_TYPE_H264, b"video")),
464                &ReceiverBatchOptions {
465                    rtp_payload_taps: vec![RtpPayloadTap {
466                        route_id: audio_route,
467                        payload_type: crate::rtp::RTP_PAYLOAD_TYPE_OPUS,
468                    }],
469                    ..ReceiverBatchOptions::default()
470                },
471            )
472            .unwrap();
473        assert_eq!(ignored.counters.raw_payload_count, 0);
474
475        let packet = rtp(crate::rtp::RTP_PAYLOAD_TYPE_OPUS, b"opus");
476        let batch = runtime
477            .push_decrypted_fragment(
478                runtime.video_runtime(),
479                1 << 8,
480                &plain(&packet),
481                &ReceiverBatchOptions {
482                    rtp_payload_taps: vec![RtpPayloadTap {
483                        route_id: audio_route,
484                        payload_type: crate::rtp::RTP_PAYLOAD_TYPE_OPUS,
485                    }],
486                    ..ReceiverBatchOptions::default()
487                },
488            )
489            .unwrap();
490
491        assert_eq!(batch.counters.raw_payload_count, 1);
492        assert_eq!(batch.raw_payloads[0].route_id, audio_route);
493        assert_eq!(batch.raw_payloads[0].data, packet);
494    }
495
496    #[test]
497    fn auxiliary_route_does_not_count_as_video_payload() {
498        let video_route = PayloadRouteId::new(1);
499        let data_route = PayloadRouteId::new(2);
500        let mut runtime = ReceiverRuntime::with_plain_video_route(
501            FrameLayout::WithFcs,
502            video_route,
503            ChannelId::default_video(),
504            0,
505            1,
506            1,
507        )
508        .unwrap();
509        let data_runtime = runtime
510            .add_plain_route(
511                data_route,
512                ChannelId::from_link_port(crate::channel::DEFAULT_LINK_ID, RadioPort::TunnelRx),
513                0,
514                1,
515                1,
516            )
517            .unwrap();
518        let batch = runtime
519            .push_decrypted_fragment(
520                data_runtime,
521                0,
522                &plain(b"data bytes"),
523                &ReceiverBatchOptions {
524                    raw_payload_routes: vec![data_route],
525                    ..ReceiverBatchOptions::default()
526                },
527            )
528            .unwrap();
529
530        assert_eq!(batch.counters.wfb_payloads, 0);
531        assert_eq!(batch.counters.rtp_packets, 0);
532        assert_eq!(batch.counters.raw_payload_count, 1);
533        assert_eq!(batch.raw_payloads[0].data, b"data bytes");
534    }
535}