Skip to main content

openipc_core/
receiver.rs

1use crate::channel::ChannelId;
2use crate::ieee80211::FrameLayout;
3use crate::realtek::{
4    parse_rx_aggregate, parse_rx_aggregate_with_kind, AggregateError, RealtekRxPacket,
5    RxDescriptorKind, RxPacketType,
6};
7use crate::routes::{
8    PayloadRouteError, PayloadRouteEvent, PayloadRouteId, PayloadRouteManager, PayloadRuntimeKey,
9};
10use crate::rtp::{
11    DepacketizedFrame, RtpDepacketizer, RtpDepacketizerStatus, RtpHeader, RtpReorderBuffer,
12    RtpReorderStatus,
13};
14use crate::wfb::{FecCounters, WfbKeypair};
15
16/// Shared receive runtime for OpenIPC video plus optional raw payload taps.
17///
18/// This is the easiest core entry point for apps. It accepts Realtek RX
19/// transfers, 802.11 frames, or already-decrypted fragments; routes recovered
20/// WFB payloads by route id; and depacketizes the configured video route from
21/// RTP into Annex-B H.264/H.265 frames.
22#[derive(Debug, Clone)]
23pub struct ReceiverRuntime {
24    routes: PayloadRouteManager,
25    video_runtime: PayloadRuntimeKey,
26    video_route_id: PayloadRouteId,
27    rtp: RtpDepacketizer,
28    rtp_reorder: Option<RtpReorderBuffer>,
29}
30
31/// Options that control how one receive batch is processed.
32#[derive(Debug, Clone)]
33pub struct ReceiverBatchOptions {
34    /// Keep CRC/ICV-marked packets instead of dropping them before WFB parsing.
35    pub accept_corrupted: bool,
36    /// Route ids whose recovered payload bytes should be copied into the batch.
37    pub raw_payload_routes: Vec<PayloadRouteId>,
38    /// RTP payload-type filters whose matching packets should be copied.
39    pub rtp_payload_taps: Vec<RtpPayloadTap>,
40    /// Depacketize the configured video route into Annex-B access units.
41    ///
42    /// Disable this when an application transfers recovered RTP to another
43    /// execution context, such as a browser decode worker. Raw route taps are
44    /// still produced while this is disabled.
45    pub depacketize_video: bool,
46}
47
48impl Default for ReceiverBatchOptions {
49    fn default() -> Self {
50        Self {
51            accept_corrupted: false,
52            raw_payload_routes: Vec::new(),
53            rtp_payload_taps: Vec::new(),
54            depacketize_video: true,
55        }
56    }
57}
58
59/// Filter for copying RTP packets from a recovered route.
60///
61/// This is useful for mixed media routes. For example, OpenIPC can carry Opus
62/// audio as RTP payload type 98 on the same WFB route as video. A tap lets an
63/// app copy only those audio RTP packets while the built-in video depacketizer
64/// continues to consume the same route for H.264/H.265.
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub struct RtpPayloadTap {
67    /// Application route id whose recovered payloads should be inspected.
68    pub route_id: PayloadRouteId,
69    /// RTP payload type to copy.
70    pub payload_type: u8,
71}
72
73/// Recovered WFB payload bytes copied for an application route.
74#[derive(Debug, Clone, PartialEq, Eq)]
75pub struct RoutePayload {
76    /// Application-defined route id that requested this payload tap.
77    pub route_id: PayloadRouteId,
78    /// WFB channel id that carried the payload.
79    pub channel_id: ChannelId,
80    /// Recovered WFB packet sequence number.
81    pub packet_seq: u64,
82    /// Raw recovered payload bytes.
83    pub data: Vec<u8>,
84}
85
86/// Counters collected while processing one receive batch.
87#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
88pub struct ReceiverBatchCounters {
89    /// Realtek RX packets seen in the batch.
90    pub packets: usize,
91    /// Packets accepted after Realtek descriptor filtering.
92    pub accepted_packets: usize,
93    /// Packets dropped by descriptor filtering.
94    pub dropped_packets: usize,
95    /// Packets dropped because the Realtek descriptor reported a CRC error.
96    pub crc_dropped: usize,
97    /// Packets dropped because the Realtek descriptor reported an ICV error.
98    pub icv_dropped: usize,
99    /// Packets dropped because they were not normal RX packets.
100    pub report_dropped: usize,
101    /// 802.11 frames that did not match any configured route or payload shape.
102    pub ignored_frames: usize,
103    /// WFB session packets accepted by configured routes.
104    pub sessions: usize,
105    /// Recovered payloads on the configured video route.
106    pub wfb_payloads: usize,
107    /// RTP packets observed on the configured video route.
108    pub rtp_packets: usize,
109    /// Annex-B frames emitted by the RTP depacketizer.
110    pub video_frames: usize,
111    /// Raw payload copies emitted for routes in [`ReceiverBatchOptions`].
112    pub raw_payload_count: usize,
113    /// Total bytes copied into raw payload outputs.
114    pub raw_payload_bytes: usize,
115    /// Route-manager errors treated as dropped/ignored frames.
116    pub route_errors: usize,
117}
118
119/// Output produced from one transfer, packet list, frame, or fragment push.
120#[derive(Debug, Clone, PartialEq, Eq)]
121pub struct ReceiverBatch {
122    /// Encoded Annex-B video frames from the configured video route.
123    pub frames: Vec<DepacketizedFrame>,
124    /// Raw payload bytes for requested route taps.
125    pub raw_payloads: Vec<RoutePayload>,
126    /// Per-batch parser and routing counters.
127    pub counters: ReceiverBatchCounters,
128    /// Current cumulative FEC counters for the video runtime.
129    pub fec_counters: FecCounters,
130    /// Current cumulative RTP depacketizer diagnostics.
131    pub rtp_status: RtpDepacketizerStatus,
132    /// Current RTP reorder-buffer diagnostics.
133    pub rtp_reorder_status: RtpReorderStatus,
134}
135
136impl ReceiverRuntime {
137    /// Build a runtime around an existing route manager.
138    ///
139    /// `video_runtime` and `video_route_id` identify the route whose recovered
140    /// payloads are RTP video and should be depacketized into frames.
141    pub fn from_routes(
142        routes: PayloadRouteManager,
143        video_runtime: PayloadRuntimeKey,
144        video_route_id: PayloadRouteId,
145    ) -> Self {
146        Self {
147            routes,
148            video_runtime,
149            video_route_id,
150            rtp: RtpDepacketizer::new(),
151            rtp_reorder: None,
152        }
153    }
154
155    /// Create a runtime with an unencrypted/plain video route.
156    ///
157    /// This is mainly useful for tests and pre-decrypted captures.
158    pub fn with_plain_video_route(
159        frame_layout: FrameLayout,
160        video_route_id: PayloadRouteId,
161        channel_id: ChannelId,
162        key_slot: u64,
163        fec_k: usize,
164        fec_n: usize,
165    ) -> Result<Self, PayloadRouteError> {
166        let mut routes = PayloadRouteManager::new(frame_layout);
167        let video_runtime =
168            routes.add_plain_route(video_route_id, channel_id, key_slot, fec_k, fec_n)?;
169        Ok(Self::from_routes(routes, video_runtime, video_route_id))
170    }
171
172    /// Create a runtime with an encrypted WFB video route.
173    pub fn with_keyed_video_route(
174        frame_layout: FrameLayout,
175        video_route_id: PayloadRouteId,
176        channel_id: ChannelId,
177        key_slot: u64,
178        keypair: WfbKeypair,
179        minimum_epoch: u64,
180    ) -> Result<Self, PayloadRouteError> {
181        let mut routes = PayloadRouteManager::new(frame_layout);
182        let video_runtime =
183            routes.add_keyed_route(video_route_id, channel_id, key_slot, keypair, minimum_epoch)?;
184        Ok(Self::from_routes(routes, video_runtime, video_route_id))
185    }
186
187    /// Create a runtime whose video route accepts already-recovered payloads.
188    ///
189    /// Use [`Self::push_direct_payload`] to inject RTP or other recovered
190    /// payload bytes. They still pass through route fanout and the built-in RTP
191    /// depacketizer, making this suitable for UDP input and no-hardware tests.
192    pub fn with_direct_video_route(
193        frame_layout: FrameLayout,
194        video_route_id: PayloadRouteId,
195        channel_id: ChannelId,
196        key_slot: u64,
197    ) -> Self {
198        let mut routes = PayloadRouteManager::new(frame_layout);
199        let video_runtime = routes.add_direct_route(video_route_id, channel_id, key_slot);
200        Self::from_routes(routes, video_runtime, video_route_id)
201    }
202
203    /// Create a synthetic video payload route for tests and development.
204    ///
205    /// This is an alias for [`Self::with_direct_video_route`].
206    pub fn with_mock_video_route(
207        frame_layout: FrameLayout,
208        video_route_id: PayloadRouteId,
209        channel_id: ChannelId,
210        key_slot: u64,
211    ) -> Self {
212        Self::with_direct_video_route(frame_layout, video_route_id, channel_id, key_slot)
213    }
214
215    /// Return the route-manager runtime key used for video.
216    pub const fn video_runtime(&self) -> PayloadRuntimeKey {
217        self.video_runtime
218    }
219
220    /// Return the application route id used for video.
221    pub const fn video_route_id(&self) -> PayloadRouteId {
222        self.video_route_id
223    }
224
225    /// Borrow the underlying route manager.
226    pub fn routes(&self) -> &PayloadRouteManager {
227        &self.routes
228    }
229
230    /// Mutably borrow the underlying route manager.
231    pub fn routes_mut(&mut self) -> &mut PayloadRouteManager {
232        &mut self.routes
233    }
234
235    /// Mutably borrow the RTP depacketizer for advanced video handling.
236    pub fn rtp_mut(&mut self) -> &mut RtpDepacketizer {
237        &mut self.rtp
238    }
239
240    /// Return cumulative RTP depacketizer diagnostics for the video route.
241    pub fn rtp_status(&self) -> RtpDepacketizerStatus {
242        self.rtp.status()
243    }
244
245    /// Return cumulative RTP reorder-buffer diagnostics for the video route.
246    pub fn rtp_reorder_status(&self) -> RtpReorderStatus {
247        self.rtp_reorder
248            .as_ref()
249            .map(RtpReorderBuffer::status)
250            .unwrap_or_default()
251    }
252
253    /// Enable or disable the small RTP sequence reorder buffer.
254    ///
255    /// Reordering can improve startup and fragmented-frame recovery on jittery
256    /// links, but it may add a tiny amount of latency when packets arrive out
257    /// of order. It is disabled by default for the lowest-latency path.
258    pub fn set_rtp_reorder_enabled(&mut self, enabled: bool) {
259        if enabled {
260            self.rtp_reorder
261                .get_or_insert_with(RtpReorderBuffer::default);
262        } else {
263            self.rtp_reorder = None;
264        }
265    }
266
267    /// Return true when RTP packets pass through the reorder buffer.
268    pub const fn rtp_reorder_enabled(&self) -> bool {
269        self.rtp_reorder.is_some()
270    }
271
272    /// Process one raw RTP packet on the configured video route.
273    pub fn push_rtp_packet(
274        &mut self,
275        packet: &[u8],
276    ) -> Result<Vec<DepacketizedFrame>, crate::rtp::RtpError> {
277        let mut frames = Vec::new();
278        self.push_video_payload_into(packet, &mut frames)?;
279        Ok(frames)
280    }
281
282    fn push_video_payload_into(
283        &mut self,
284        payload: &[u8],
285        frames: &mut Vec<DepacketizedFrame>,
286    ) -> Result<usize, crate::rtp::RtpError> {
287        let before = frames.len();
288        if let Some(reorder) = self.rtp_reorder.as_mut() {
289            for ordered in reorder.push(payload)? {
290                if let Some(frame) = self.rtp.push(&ordered)? {
291                    frames.push(frame);
292                }
293            }
294        } else if let Some(frame) = self.rtp.push(payload)? {
295            frames.push(frame);
296        }
297        Ok(frames.len() - before)
298    }
299
300    /// Add an unencrypted/plain raw-payload route.
301    pub fn add_plain_route(
302        &mut self,
303        route_id: PayloadRouteId,
304        channel_id: ChannelId,
305        key_slot: u64,
306        fec_k: usize,
307        fec_n: usize,
308    ) -> Result<PayloadRuntimeKey, PayloadRouteError> {
309        self.routes
310            .add_plain_route(route_id, channel_id, key_slot, fec_k, fec_n)
311    }
312
313    /// Add an encrypted WFB raw-payload route.
314    pub fn add_keyed_route(
315        &mut self,
316        route_id: PayloadRouteId,
317        channel_id: ChannelId,
318        key_slot: u64,
319        keypair: WfbKeypair,
320        minimum_epoch: u64,
321    ) -> Result<PayloadRuntimeKey, PayloadRouteError> {
322        self.routes
323            .add_keyed_route(route_id, channel_id, key_slot, keypair, minimum_epoch)
324    }
325
326    /// Add a route that accepts already-recovered payloads directly.
327    pub fn add_direct_route(
328        &mut self,
329        route_id: PayloadRouteId,
330        channel_id: ChannelId,
331        key_slot: u64,
332    ) -> PayloadRuntimeKey {
333        self.routes.add_direct_route(route_id, channel_id, key_slot)
334    }
335
336    /// Add a synthetic raw-payload route for tests and development.
337    ///
338    /// This is an alias for [`Self::add_direct_route`].
339    pub fn add_mock_route(
340        &mut self,
341        route_id: PayloadRouteId,
342        channel_id: ChannelId,
343        key_slot: u64,
344    ) -> PayloadRuntimeKey {
345        self.add_direct_route(route_id, channel_id, key_slot)
346    }
347
348    /// Return cumulative FEC counters for the video runtime.
349    pub fn video_fec_counters(&self) -> FecCounters {
350        self.routes
351            .fec_counters(self.video_runtime)
352            .unwrap_or_default()
353    }
354
355    /// Return true if an 802.11 frame belongs to the configured video runtime.
356    pub fn accepts_video_frame(&self, frame: &[u8]) -> bool {
357        self.routes.accepts_80211_frame(self.video_runtime, frame)
358    }
359
360    /// Parse and process one Realtek USB RX transfer.
361    pub fn push_rx_transfer(
362        &mut self,
363        transfer: &[u8],
364        options: &ReceiverBatchOptions,
365    ) -> Result<ReceiverBatch, AggregateError> {
366        let packets = parse_rx_aggregate(transfer)?;
367        Ok(self.push_rx_packets(packets, options))
368    }
369
370    /// Parse and process one Realtek USB RX transfer with an explicit descriptor layout.
371    ///
372    /// Use the layout reported by the hardware driver for Jaguar3 adapters.
373    /// [`Self::push_rx_transfer`] remains the Jaguar1-compatible convenience method.
374    pub fn push_rx_transfer_with_kind(
375        &mut self,
376        transfer: &[u8],
377        descriptor_kind: RxDescriptorKind,
378        options: &ReceiverBatchOptions,
379    ) -> Result<ReceiverBatch, AggregateError> {
380        let packets = parse_rx_aggregate_with_kind(transfer, descriptor_kind)?;
381        Ok(self.push_rx_packets(packets, options))
382    }
383
384    /// Process already parsed Realtek RX packets.
385    pub fn push_rx_packets<'a>(
386        &mut self,
387        packets: impl IntoIterator<Item = RealtekRxPacket<'a>>,
388        options: &ReceiverBatchOptions,
389    ) -> ReceiverBatch {
390        let mut batch = self.empty_batch();
391
392        for packet in packets {
393            batch.counters.packets += 1;
394            if packet.attrib.crc_err && !options.accept_corrupted {
395                batch.counters.crc_dropped += 1;
396                continue;
397            }
398            if packet.attrib.icv_err && !options.accept_corrupted {
399                batch.counters.icv_dropped += 1;
400                continue;
401            }
402            if packet.attrib.pkt_rpt_type != RxPacketType::NormalRx {
403                batch.counters.report_dropped += 1;
404                continue;
405            }
406
407            batch.counters.accepted_packets += 1;
408            match self.routes.push_80211_frame(packet.data) {
409                Ok(events) => self.apply_route_events(events, options, &mut batch),
410                Err(_) => {
411                    batch.counters.ignored_frames += 1;
412                    batch.counters.route_errors += 1;
413                }
414            }
415        }
416
417        batch.counters.dropped_packets =
418            batch.counters.crc_dropped + batch.counters.icv_dropped + batch.counters.report_dropped;
419        batch.fec_counters = self.video_fec_counters();
420        batch
421    }
422
423    /// Process one OpenIPC/WFB 802.11 frame.
424    pub fn push_80211_frame(
425        &mut self,
426        frame: &[u8],
427        options: &ReceiverBatchOptions,
428    ) -> Result<ReceiverBatch, PayloadRouteError> {
429        let mut batch = self.empty_batch();
430        let events = self.routes.push_80211_frame(frame)?;
431        self.apply_route_events(events, options, &mut batch);
432        batch.fec_counters = self.video_fec_counters();
433        Ok(batch)
434    }
435
436    /// Process one 802.11 frame when the caller already decrypted the WFB fragment.
437    pub fn push_decrypted_80211_frame(
438        &mut self,
439        runtime: PayloadRuntimeKey,
440        frame: &[u8],
441        decrypted_fragment: &[u8],
442        options: &ReceiverBatchOptions,
443    ) -> Result<ReceiverBatch, PayloadRouteError> {
444        let mut batch = self.empty_batch();
445        let events = self
446            .routes
447            .push_decrypted_80211_frame(runtime, frame, decrypted_fragment)?;
448        self.apply_route_events(events, options, &mut batch);
449        batch.fec_counters = self.video_fec_counters();
450        Ok(batch)
451    }
452
453    /// Process one already-decrypted WFB fragment.
454    pub fn push_decrypted_fragment(
455        &mut self,
456        runtime: PayloadRuntimeKey,
457        data_nonce: u64,
458        decrypted_fragment: &[u8],
459        options: &ReceiverBatchOptions,
460    ) -> Result<ReceiverBatch, PayloadRouteError> {
461        let mut batch = self.empty_batch();
462        let events =
463            self.routes
464                .push_decrypted_fragment(runtime, data_nonce, decrypted_fragment)?;
465        self.apply_route_events(events, options, &mut batch);
466        batch.fec_counters = self.video_fec_counters();
467        Ok(batch)
468    }
469
470    /// Process one already-recovered payload through routes and RTP handling.
471    pub fn push_direct_payload(
472        &mut self,
473        runtime: PayloadRuntimeKey,
474        packet_seq: u64,
475        payload: &[u8],
476        options: &ReceiverBatchOptions,
477    ) -> Result<ReceiverBatch, PayloadRouteError> {
478        let mut batch = self.empty_batch();
479        let events = self
480            .routes
481            .push_direct_payload(runtime, packet_seq, payload)?;
482        self.apply_route_events(events, options, &mut batch);
483        batch.fec_counters = self.video_fec_counters();
484        Ok(batch)
485    }
486
487    /// Process one synthetic recovered payload for tests and development.
488    ///
489    /// This is an alias for [`Self::push_direct_payload`].
490    pub fn push_mock_payload(
491        &mut self,
492        runtime: PayloadRuntimeKey,
493        packet_seq: u64,
494        payload: &[u8],
495        options: &ReceiverBatchOptions,
496    ) -> Result<ReceiverBatch, PayloadRouteError> {
497        self.push_direct_payload(runtime, packet_seq, payload, options)
498    }
499
500    fn empty_batch(&self) -> ReceiverBatch {
501        ReceiverBatch {
502            frames: Vec::new(),
503            raw_payloads: Vec::new(),
504            counters: ReceiverBatchCounters::default(),
505            fec_counters: self.video_fec_counters(),
506            rtp_status: self.rtp_status(),
507            rtp_reorder_status: self.rtp_reorder_status(),
508        }
509    }
510
511    fn apply_route_events(
512        &mut self,
513        events: Vec<PayloadRouteEvent>,
514        options: &ReceiverBatchOptions,
515        batch: &mut ReceiverBatch,
516    ) {
517        for event in events {
518            match event {
519                PayloadRouteEvent::IgnoredFrame => batch.counters.ignored_frames += 1,
520                PayloadRouteEvent::SessionEstablished { .. } => batch.counters.sessions += 1,
521                PayloadRouteEvent::Payload {
522                    route_ids, payload, ..
523                } => {
524                    if route_ids.contains(&self.video_route_id) {
525                        batch.counters.wfb_payloads += 1;
526                        batch.counters.rtp_packets += 1;
527                        if options.depacketize_video {
528                            if let Ok(frames) =
529                                self.push_video_payload_into(&payload.data, &mut batch.frames)
530                            {
531                                batch.counters.video_frames += frames;
532                            }
533                        }
534                    }
535
536                    for &route_id in &route_ids {
537                        if !options.raw_payload_routes.contains(&route_id) {
538                            continue;
539                        }
540                        copy_raw_payload(route_id, &payload, batch);
541                    }
542
543                    if options.rtp_payload_taps.is_empty() {
544                        continue;
545                    }
546                    let Ok(header) = RtpHeader::parse(&payload.data) else {
547                        continue;
548                    };
549                    for tap in &options.rtp_payload_taps {
550                        if header.payload_type != tap.payload_type {
551                            continue;
552                        }
553                        if !route_ids.contains(&tap.route_id) {
554                            continue;
555                        }
556                        copy_raw_payload(tap.route_id, &payload, batch);
557                    }
558                }
559            }
560        }
561        batch.rtp_status = self.rtp_status();
562        batch.rtp_reorder_status = self.rtp_reorder_status();
563    }
564}
565
566fn copy_raw_payload(
567    route_id: PayloadRouteId,
568    payload: &crate::pipeline::RecoveredPayload,
569    batch: &mut ReceiverBatch,
570) {
571    batch.counters.raw_payload_count += 1;
572    batch.counters.raw_payload_bytes += payload.data.len();
573    batch.raw_payloads.push(RoutePayload {
574        route_id,
575        channel_id: payload.channel_id,
576        packet_seq: payload.packet_seq,
577        data: payload.data.clone(),
578    });
579}
580
581#[cfg(test)]
582mod tests {
583    use super::*;
584    use crate::RadioPort;
585
586    fn plain(payload: &[u8]) -> Vec<u8> {
587        let mut out = Vec::new();
588        out.push(0);
589        out.extend_from_slice(&(payload.len() as u16).to_be_bytes());
590        out.extend_from_slice(payload);
591        out
592    }
593
594    fn rtp(payload_type: u8, payload: &[u8]) -> Vec<u8> {
595        let mut packet = vec![0x80, payload_type & 0x7f];
596        packet.extend_from_slice(&7u16.to_be_bytes());
597        packet.extend_from_slice(&48_000u32.to_be_bytes());
598        packet.extend_from_slice(&0x1122_3344u32.to_be_bytes());
599        packet.extend_from_slice(payload);
600        packet
601    }
602
603    fn h264_stap_a_rtp() -> Vec<u8> {
604        let sps = [0x67, 0x42, 0x00, 0x1e, 0xab];
605        let pps = [0x68, 0xce, 0x06, 0xe2];
606        let idr = [0x65, 0x88, 0x84, 0x21];
607        let mut payload = vec![24];
608        for nalu in [&sps[..], &pps[..], &idr[..]] {
609            payload.extend_from_slice(&(nalu.len() as u16).to_be_bytes());
610            payload.extend_from_slice(nalu);
611        }
612        let mut packet = rtp(crate::rtp::RTP_PAYLOAD_TYPE_H264, &payload);
613        packet[1] |= 0x80;
614        packet
615    }
616
617    #[test]
618    fn decrypted_fragment_can_fan_out_to_raw_route() {
619        let route = PayloadRouteId::new(7);
620        let mut runtime = ReceiverRuntime::with_plain_video_route(
621            FrameLayout::WithFcs,
622            route,
623            ChannelId::default_video(),
624            0,
625            1,
626            1,
627        )
628        .unwrap();
629        let batch = runtime
630            .push_decrypted_fragment(
631                runtime.video_runtime(),
632                0,
633                &plain(b"payload bytes"),
634                &ReceiverBatchOptions {
635                    raw_payload_routes: vec![route],
636                    ..ReceiverBatchOptions::default()
637                },
638            )
639            .unwrap();
640
641        assert_eq!(batch.counters.wfb_payloads, 1);
642        assert_eq!(batch.counters.raw_payload_count, 1);
643        assert_eq!(batch.raw_payloads[0].data, b"payload bytes");
644    }
645
646    #[test]
647    fn rtp_payload_tap_copies_only_matching_payload_type() {
648        let video_route = PayloadRouteId::new(1);
649        let audio_route = PayloadRouteId::new(3);
650        let mut runtime = ReceiverRuntime::with_plain_video_route(
651            FrameLayout::WithFcs,
652            video_route,
653            ChannelId::default_video(),
654            0,
655            1,
656            1,
657        )
658        .unwrap();
659        runtime
660            .add_plain_route(audio_route, ChannelId::default_video(), 0, 1, 1)
661            .unwrap();
662
663        let ignored = runtime
664            .push_decrypted_fragment(
665                runtime.video_runtime(),
666                0,
667                &plain(&rtp(crate::rtp::RTP_PAYLOAD_TYPE_H264, b"video")),
668                &ReceiverBatchOptions {
669                    rtp_payload_taps: vec![RtpPayloadTap {
670                        route_id: audio_route,
671                        payload_type: crate::rtp::RTP_PAYLOAD_TYPE_OPUS,
672                    }],
673                    ..ReceiverBatchOptions::default()
674                },
675            )
676            .unwrap();
677        assert_eq!(ignored.counters.raw_payload_count, 0);
678
679        let packet = rtp(crate::rtp::RTP_PAYLOAD_TYPE_OPUS, b"opus");
680        let batch = runtime
681            .push_decrypted_fragment(
682                runtime.video_runtime(),
683                1 << 8,
684                &plain(&packet),
685                &ReceiverBatchOptions {
686                    rtp_payload_taps: vec![RtpPayloadTap {
687                        route_id: audio_route,
688                        payload_type: crate::rtp::RTP_PAYLOAD_TYPE_OPUS,
689                    }],
690                    ..ReceiverBatchOptions::default()
691                },
692            )
693            .unwrap();
694
695        assert_eq!(batch.counters.raw_payload_count, 1);
696        assert_eq!(batch.raw_payloads[0].route_id, audio_route);
697        assert_eq!(batch.raw_payloads[0].data, packet);
698    }
699
700    #[test]
701    fn rtp_reorder_is_opt_in() {
702        let mut runtime = ReceiverRuntime::with_plain_video_route(
703            FrameLayout::WithFcs,
704            PayloadRouteId::new(1),
705            ChannelId::default_video(),
706            0,
707            1,
708            1,
709        )
710        .unwrap();
711
712        assert!(!runtime.rtp_reorder_enabled());
713        assert_eq!(runtime.rtp_reorder_status(), RtpReorderStatus::default());
714
715        runtime.set_rtp_reorder_enabled(true);
716        assert!(runtime.rtp_reorder_enabled());
717
718        runtime.set_rtp_reorder_enabled(false);
719        assert!(!runtime.rtp_reorder_enabled());
720        assert_eq!(runtime.rtp_reorder_status(), RtpReorderStatus::default());
721    }
722
723    #[test]
724    fn auxiliary_route_does_not_count_as_video_payload() {
725        let video_route = PayloadRouteId::new(1);
726        let data_route = PayloadRouteId::new(2);
727        let mut runtime = ReceiverRuntime::with_plain_video_route(
728            FrameLayout::WithFcs,
729            video_route,
730            ChannelId::default_video(),
731            0,
732            1,
733            1,
734        )
735        .unwrap();
736        let data_runtime = runtime
737            .add_plain_route(
738                data_route,
739                ChannelId::from_link_port(crate::channel::DEFAULT_LINK_ID, RadioPort::TunnelRx),
740                0,
741                1,
742                1,
743            )
744            .unwrap();
745        let batch = runtime
746            .push_decrypted_fragment(
747                data_runtime,
748                0,
749                &plain(b"data bytes"),
750                &ReceiverBatchOptions {
751                    raw_payload_routes: vec![data_route],
752                    ..ReceiverBatchOptions::default()
753                },
754            )
755            .unwrap();
756
757        assert_eq!(batch.counters.wfb_payloads, 0);
758        assert_eq!(batch.counters.rtp_packets, 0);
759        assert_eq!(batch.counters.raw_payload_count, 1);
760        assert_eq!(batch.raw_payloads[0].data, b"data bytes");
761    }
762
763    #[test]
764    fn direct_payload_runtime_uses_same_video_route_and_rtp_depacketizer() {
765        let video_route = PayloadRouteId::new(1);
766        let mut runtime = ReceiverRuntime::with_direct_video_route(
767            FrameLayout::WithFcs,
768            video_route,
769            ChannelId::default_video(),
770            0,
771        );
772
773        let packet = h264_stap_a_rtp();
774        let batch = runtime
775            .push_direct_payload(
776                runtime.video_runtime(),
777                123,
778                &packet,
779                &ReceiverBatchOptions {
780                    raw_payload_routes: vec![video_route],
781                    ..ReceiverBatchOptions::default()
782                },
783            )
784            .unwrap();
785
786        assert_eq!(batch.counters.wfb_payloads, 1);
787        assert_eq!(batch.counters.rtp_packets, 1);
788        assert_eq!(batch.counters.video_frames, 1);
789        assert_eq!(batch.frames.len(), 1);
790        assert_eq!(batch.frames[0].codec, crate::rtp::Codec::H264);
791        assert!(batch.frames[0].is_keyframe);
792        assert_eq!(batch.raw_payloads[0].data, packet);
793        assert_eq!(batch.fec_counters, FecCounters::default());
794    }
795
796    #[test]
797    fn video_depacketization_can_be_delegated_without_losing_raw_rtp() {
798        let video_route = PayloadRouteId::new(1);
799        let mut runtime = ReceiverRuntime::with_direct_video_route(
800            FrameLayout::WithFcs,
801            video_route,
802            ChannelId::default_video(),
803            0,
804        );
805        let packet = h264_stap_a_rtp();
806
807        let batch = runtime
808            .push_direct_payload(
809                runtime.video_runtime(),
810                123,
811                &packet,
812                &ReceiverBatchOptions {
813                    raw_payload_routes: vec![video_route],
814                    depacketize_video: false,
815                    ..ReceiverBatchOptions::default()
816                },
817            )
818            .unwrap();
819
820        assert_eq!(batch.counters.wfb_payloads, 1);
821        assert_eq!(batch.counters.rtp_packets, 1);
822        assert_eq!(batch.counters.video_frames, 0);
823        assert!(batch.frames.is_empty());
824        assert_eq!(batch.raw_payloads[0].data, packet);
825        assert_eq!(batch.rtp_status, RtpDepacketizerStatus::default());
826    }
827
828    #[test]
829    fn rx_transfer_accepts_explicit_jaguar3_descriptor_layout() {
830        let mut runtime = ReceiverRuntime::with_plain_video_route(
831            FrameLayout::WithFcs,
832            PayloadRouteId::new(1),
833            ChannelId::default_video(),
834            0,
835            1,
836            1,
837        )
838        .unwrap();
839        let mut transfer = vec![0u8; 32];
840        transfer[..4].copy_from_slice(&8u32.to_le_bytes());
841        transfer[24..32].copy_from_slice(&[0x08, 0, 0, 0, 0, 0, 0, 0]);
842
843        let batch = runtime
844            .push_rx_transfer_with_kind(
845                &transfer,
846                RxDescriptorKind::Jaguar3,
847                &ReceiverBatchOptions::default(),
848            )
849            .unwrap();
850
851        assert_eq!(batch.counters.packets, 1);
852        assert_eq!(batch.counters.accepted_packets, 1);
853        assert_eq!(batch.counters.ignored_frames, 1);
854    }
855}