Skip to main content

openipc_core/
receiver.rs

1use crate::channel::ChannelId;
2use crate::ieee80211::{FrameLayout, WifiFrame};
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        let mut route_events = Vec::new();
392
393        for packet in packets {
394            let parsed = if packet.attrib.crc_err
395                || packet.attrib.icv_err
396                || packet.attrib.pkt_rpt_type != RxPacketType::NormalRx
397            {
398                None
399            } else {
400                WifiFrame::parse(packet.data, self.routes.frame_layout()).ok()
401            };
402            self.push_rx_packet(packet, parsed, options, &mut route_events, &mut batch);
403        }
404
405        self.finish_rx_batch(batch)
406    }
407
408    /// Process RX packets accompanied by an already-validated WiFi view.
409    ///
410    /// This avoids reparsing frames that an application already inspected for
411    /// diversity selection or signal accounting.
412    pub fn push_parsed_rx_packets<'a>(
413        &mut self,
414        packets: impl IntoIterator<Item = (RealtekRxPacket<'a>, Option<WifiFrame<'a>>)>,
415        options: &ReceiverBatchOptions,
416    ) -> ReceiverBatch {
417        let mut batch = self.empty_batch();
418        let mut route_events = Vec::new();
419        for (packet, parsed) in packets {
420            self.push_rx_packet(packet, parsed, options, &mut route_events, &mut batch);
421        }
422        self.finish_rx_batch(batch)
423    }
424
425    fn push_rx_packet(
426        &mut self,
427        packet: RealtekRxPacket<'_>,
428        parsed: Option<WifiFrame<'_>>,
429        options: &ReceiverBatchOptions,
430        route_events: &mut Vec<PayloadRouteEvent>,
431        batch: &mut ReceiverBatch,
432    ) {
433        batch.counters.packets += 1;
434        if packet.attrib.crc_err && !options.accept_corrupted {
435            batch.counters.crc_dropped += 1;
436            return;
437        }
438        if packet.attrib.icv_err && !options.accept_corrupted {
439            batch.counters.icv_dropped += 1;
440            return;
441        }
442        if packet.attrib.pkt_rpt_type != RxPacketType::NormalRx {
443            batch.counters.report_dropped += 1;
444            return;
445        }
446
447        batch.counters.accepted_packets += 1;
448        let Some(parsed) = parsed else {
449            batch.counters.ignored_frames += 1;
450            return;
451        };
452        match self.routes.push_wifi_frame_into(parsed, route_events) {
453            Ok(()) => self.apply_route_events(route_events.drain(..), options, batch),
454            Err(_) => {
455                batch.counters.ignored_frames += 1;
456                batch.counters.route_errors += 1;
457            }
458        }
459    }
460
461    fn finish_rx_batch(&self, mut batch: ReceiverBatch) -> ReceiverBatch {
462        batch.counters.dropped_packets =
463            batch.counters.crc_dropped + batch.counters.icv_dropped + batch.counters.report_dropped;
464        batch.fec_counters = self.video_fec_counters();
465        batch
466    }
467
468    /// Process one OpenIPC/WFB 802.11 frame.
469    pub fn push_80211_frame(
470        &mut self,
471        frame: &[u8],
472        options: &ReceiverBatchOptions,
473    ) -> Result<ReceiverBatch, PayloadRouteError> {
474        let mut batch = self.empty_batch();
475        let events = self.routes.push_80211_frame(frame)?;
476        self.apply_route_events(events, options, &mut batch);
477        batch.fec_counters = self.video_fec_counters();
478        Ok(batch)
479    }
480
481    /// Process one 802.11 frame when the caller already decrypted the WFB fragment.
482    pub fn push_decrypted_80211_frame(
483        &mut self,
484        runtime: PayloadRuntimeKey,
485        frame: &[u8],
486        decrypted_fragment: &[u8],
487        options: &ReceiverBatchOptions,
488    ) -> Result<ReceiverBatch, PayloadRouteError> {
489        let mut batch = self.empty_batch();
490        let events = self
491            .routes
492            .push_decrypted_80211_frame(runtime, frame, decrypted_fragment)?;
493        self.apply_route_events(events, options, &mut batch);
494        batch.fec_counters = self.video_fec_counters();
495        Ok(batch)
496    }
497
498    /// Process one already-decrypted WFB fragment.
499    pub fn push_decrypted_fragment(
500        &mut self,
501        runtime: PayloadRuntimeKey,
502        data_nonce: u64,
503        decrypted_fragment: &[u8],
504        options: &ReceiverBatchOptions,
505    ) -> Result<ReceiverBatch, PayloadRouteError> {
506        let mut batch = self.empty_batch();
507        let events =
508            self.routes
509                .push_decrypted_fragment(runtime, data_nonce, decrypted_fragment)?;
510        self.apply_route_events(events, options, &mut batch);
511        batch.fec_counters = self.video_fec_counters();
512        Ok(batch)
513    }
514
515    /// Process one already-recovered payload through routes and RTP handling.
516    pub fn push_direct_payload(
517        &mut self,
518        runtime: PayloadRuntimeKey,
519        packet_seq: u64,
520        payload: &[u8],
521        options: &ReceiverBatchOptions,
522    ) -> Result<ReceiverBatch, PayloadRouteError> {
523        let mut batch = self.empty_batch();
524        let (route_ids, direct) = self
525            .routes
526            .route_membership(runtime)
527            .ok_or(PayloadRouteError::UnknownRuntime(runtime))?;
528        if direct {
529            self.apply_recovered_payload(
530                &route_ids,
531                runtime.channel_id(),
532                packet_seq,
533                payload,
534                options,
535                &mut batch,
536            );
537        } else {
538            batch.counters.ignored_frames += 1;
539        }
540        batch.fec_counters = self.video_fec_counters();
541        batch.rtp_status = self.rtp_status();
542        batch.rtp_reorder_status = self.rtp_reorder_status();
543        Ok(batch)
544    }
545
546    /// Process one synthetic recovered payload for tests and development.
547    ///
548    /// This is an alias for [`Self::push_direct_payload`].
549    pub fn push_mock_payload(
550        &mut self,
551        runtime: PayloadRuntimeKey,
552        packet_seq: u64,
553        payload: &[u8],
554        options: &ReceiverBatchOptions,
555    ) -> Result<ReceiverBatch, PayloadRouteError> {
556        self.push_direct_payload(runtime, packet_seq, payload, options)
557    }
558
559    fn empty_batch(&self) -> ReceiverBatch {
560        ReceiverBatch {
561            frames: Vec::new(),
562            raw_payloads: Vec::new(),
563            counters: ReceiverBatchCounters::default(),
564            fec_counters: self.video_fec_counters(),
565            rtp_status: self.rtp_status(),
566            rtp_reorder_status: self.rtp_reorder_status(),
567        }
568    }
569
570    fn apply_route_events(
571        &mut self,
572        events: impl IntoIterator<Item = PayloadRouteEvent>,
573        options: &ReceiverBatchOptions,
574        batch: &mut ReceiverBatch,
575    ) {
576        for event in events {
577            match event {
578                PayloadRouteEvent::IgnoredFrame => batch.counters.ignored_frames += 1,
579                PayloadRouteEvent::SessionEstablished { .. } => batch.counters.sessions += 1,
580                PayloadRouteEvent::Payload {
581                    route_ids, payload, ..
582                } => {
583                    self.apply_owned_recovered_payload(
584                        &route_ids,
585                        payload.channel_id,
586                        payload.packet_seq,
587                        payload.data,
588                        options,
589                        batch,
590                    );
591                }
592            }
593        }
594        batch.rtp_status = self.rtp_status();
595        batch.rtp_reorder_status = self.rtp_reorder_status();
596    }
597
598    fn apply_recovered_payload(
599        &mut self,
600        route_ids: &[PayloadRouteId],
601        channel_id: ChannelId,
602        packet_seq: u64,
603        data: &[u8],
604        options: &ReceiverBatchOptions,
605        batch: &mut ReceiverBatch,
606    ) {
607        self.apply_video_payload(route_ids, data, options, batch);
608
609        for &route_id in route_ids {
610            if options.raw_payload_routes.contains(&route_id) {
611                copy_raw_payload(route_id, channel_id, packet_seq, data, batch);
612            }
613        }
614
615        if options.rtp_payload_taps.is_empty() {
616            return;
617        }
618        let Ok(header) = RtpHeader::parse(data) else {
619            return;
620        };
621        for tap in &options.rtp_payload_taps {
622            if header.payload_type == tap.payload_type && route_ids.contains(&tap.route_id) {
623                copy_raw_payload(tap.route_id, channel_id, packet_seq, data, batch);
624            }
625        }
626    }
627
628    fn apply_owned_recovered_payload(
629        &mut self,
630        route_ids: &[PayloadRouteId],
631        channel_id: ChannelId,
632        packet_seq: u64,
633        data: Vec<u8>,
634        options: &ReceiverBatchOptions,
635        batch: &mut ReceiverBatch,
636    ) {
637        self.apply_video_payload(route_ids, &data, options, batch);
638
639        let payload_type = (!options.rtp_payload_taps.is_empty())
640            .then(|| {
641                RtpHeader::parse(&data)
642                    .ok()
643                    .map(|header| header.payload_type)
644            })
645            .flatten();
646        let mut remaining = route_ids
647            .iter()
648            .filter(|route_id| options.raw_payload_routes.contains(route_id))
649            .count()
650            + options
651                .rtp_payload_taps
652                .iter()
653                .filter(|tap| {
654                    payload_type == Some(tap.payload_type) && route_ids.contains(&tap.route_id)
655                })
656                .count();
657        if remaining == 0 {
658            return;
659        }
660
661        let mut data = Some(data);
662        for &route_id in route_ids {
663            if options.raw_payload_routes.contains(&route_id) {
664                remaining -= 1;
665                push_raw_payload(
666                    route_id,
667                    channel_id,
668                    packet_seq,
669                    take_or_clone_payload(&mut data, remaining),
670                    batch,
671                );
672            }
673        }
674        for tap in &options.rtp_payload_taps {
675            if payload_type == Some(tap.payload_type) && route_ids.contains(&tap.route_id) {
676                remaining -= 1;
677                push_raw_payload(
678                    tap.route_id,
679                    channel_id,
680                    packet_seq,
681                    take_or_clone_payload(&mut data, remaining),
682                    batch,
683                );
684            }
685        }
686    }
687
688    fn apply_video_payload(
689        &mut self,
690        route_ids: &[PayloadRouteId],
691        data: &[u8],
692        options: &ReceiverBatchOptions,
693        batch: &mut ReceiverBatch,
694    ) {
695        if route_ids.contains(&self.video_route_id) {
696            batch.counters.wfb_payloads += 1;
697            batch.counters.rtp_packets += 1;
698            if options.depacketize_video {
699                if let Ok(frames) = self.push_video_payload_into(data, &mut batch.frames) {
700                    batch.counters.video_frames += frames;
701                }
702            }
703        }
704    }
705}
706
707fn take_or_clone_payload(data: &mut Option<Vec<u8>>, remaining: usize) -> Vec<u8> {
708    if remaining == 0 {
709        data.take()
710            .expect("the final raw route must own the recovered payload")
711    } else {
712        data.as_ref()
713            .expect("raw route payload must remain available")
714            .clone()
715    }
716}
717
718fn copy_raw_payload(
719    route_id: PayloadRouteId,
720    channel_id: ChannelId,
721    packet_seq: u64,
722    data: &[u8],
723    batch: &mut ReceiverBatch,
724) {
725    push_raw_payload(route_id, channel_id, packet_seq, data.to_vec(), batch);
726}
727
728fn push_raw_payload(
729    route_id: PayloadRouteId,
730    channel_id: ChannelId,
731    packet_seq: u64,
732    data: Vec<u8>,
733    batch: &mut ReceiverBatch,
734) {
735    batch.counters.raw_payload_count += 1;
736    batch.counters.raw_payload_bytes += data.len();
737    batch.raw_payloads.push(RoutePayload {
738        route_id,
739        channel_id,
740        packet_seq,
741        data,
742    });
743}
744
745#[cfg(test)]
746mod tests {
747    use super::*;
748    use crate::RadioPort;
749
750    fn plain(payload: &[u8]) -> Vec<u8> {
751        let mut out = Vec::new();
752        out.push(0);
753        out.extend_from_slice(&(payload.len() as u16).to_be_bytes());
754        out.extend_from_slice(payload);
755        out
756    }
757
758    fn rtp(payload_type: u8, payload: &[u8]) -> Vec<u8> {
759        let mut packet = vec![0x80, payload_type & 0x7f];
760        packet.extend_from_slice(&7u16.to_be_bytes());
761        packet.extend_from_slice(&48_000u32.to_be_bytes());
762        packet.extend_from_slice(&0x1122_3344u32.to_be_bytes());
763        packet.extend_from_slice(payload);
764        packet
765    }
766
767    fn h264_stap_a_rtp() -> Vec<u8> {
768        let sps = [0x67, 0x42, 0x00, 0x1e, 0xab];
769        let pps = [0x68, 0xce, 0x06, 0xe2];
770        let idr = [0x65, 0x88, 0x84, 0x21];
771        let mut payload = vec![24];
772        for nalu in [&sps[..], &pps[..], &idr[..]] {
773            payload.extend_from_slice(&(nalu.len() as u16).to_be_bytes());
774            payload.extend_from_slice(nalu);
775        }
776        let mut packet = rtp(crate::rtp::RTP_PAYLOAD_TYPE_H264, &payload);
777        packet[1] |= 0x80;
778        packet
779    }
780
781    #[test]
782    fn decrypted_fragment_can_fan_out_to_raw_route() {
783        let route = PayloadRouteId::new(7);
784        let mut runtime = ReceiverRuntime::with_plain_video_route(
785            FrameLayout::WithFcs,
786            route,
787            ChannelId::default_video(),
788            0,
789            1,
790            1,
791        )
792        .unwrap();
793        let batch = runtime
794            .push_decrypted_fragment(
795                runtime.video_runtime(),
796                0,
797                &plain(b"payload bytes"),
798                &ReceiverBatchOptions {
799                    raw_payload_routes: vec![route],
800                    ..ReceiverBatchOptions::default()
801                },
802            )
803            .unwrap();
804
805        assert_eq!(batch.counters.wfb_payloads, 1);
806        assert_eq!(batch.counters.raw_payload_count, 1);
807        assert_eq!(batch.raw_payloads[0].data, b"payload bytes");
808    }
809
810    #[test]
811    fn rtp_payload_tap_copies_only_matching_payload_type() {
812        let video_route = PayloadRouteId::new(1);
813        let audio_route = PayloadRouteId::new(3);
814        let mut runtime = ReceiverRuntime::with_plain_video_route(
815            FrameLayout::WithFcs,
816            video_route,
817            ChannelId::default_video(),
818            0,
819            1,
820            1,
821        )
822        .unwrap();
823        runtime
824            .add_plain_route(audio_route, ChannelId::default_video(), 0, 1, 1)
825            .unwrap();
826
827        let ignored = runtime
828            .push_decrypted_fragment(
829                runtime.video_runtime(),
830                0,
831                &plain(&rtp(crate::rtp::RTP_PAYLOAD_TYPE_H264, b"video")),
832                &ReceiverBatchOptions {
833                    rtp_payload_taps: vec![RtpPayloadTap {
834                        route_id: audio_route,
835                        payload_type: crate::rtp::RTP_PAYLOAD_TYPE_OPUS,
836                    }],
837                    ..ReceiverBatchOptions::default()
838                },
839            )
840            .unwrap();
841        assert_eq!(ignored.counters.raw_payload_count, 0);
842
843        let packet = rtp(crate::rtp::RTP_PAYLOAD_TYPE_OPUS, b"opus");
844        let batch = runtime
845            .push_decrypted_fragment(
846                runtime.video_runtime(),
847                1 << 8,
848                &plain(&packet),
849                &ReceiverBatchOptions {
850                    rtp_payload_taps: vec![RtpPayloadTap {
851                        route_id: audio_route,
852                        payload_type: crate::rtp::RTP_PAYLOAD_TYPE_OPUS,
853                    }],
854                    ..ReceiverBatchOptions::default()
855                },
856            )
857            .unwrap();
858
859        assert_eq!(batch.counters.raw_payload_count, 1);
860        assert_eq!(batch.raw_payloads[0].route_id, audio_route);
861        assert_eq!(batch.raw_payloads[0].data, packet);
862    }
863
864    #[test]
865    fn rtp_reorder_is_opt_in() {
866        let mut runtime = ReceiverRuntime::with_plain_video_route(
867            FrameLayout::WithFcs,
868            PayloadRouteId::new(1),
869            ChannelId::default_video(),
870            0,
871            1,
872            1,
873        )
874        .unwrap();
875
876        assert!(!runtime.rtp_reorder_enabled());
877        assert_eq!(runtime.rtp_reorder_status(), RtpReorderStatus::default());
878
879        runtime.set_rtp_reorder_enabled(true);
880        assert!(runtime.rtp_reorder_enabled());
881
882        runtime.set_rtp_reorder_enabled(false);
883        assert!(!runtime.rtp_reorder_enabled());
884        assert_eq!(runtime.rtp_reorder_status(), RtpReorderStatus::default());
885    }
886
887    #[test]
888    fn auxiliary_route_does_not_count_as_video_payload() {
889        let video_route = PayloadRouteId::new(1);
890        let data_route = PayloadRouteId::new(2);
891        let mut runtime = ReceiverRuntime::with_plain_video_route(
892            FrameLayout::WithFcs,
893            video_route,
894            ChannelId::default_video(),
895            0,
896            1,
897            1,
898        )
899        .unwrap();
900        let data_runtime = runtime
901            .add_plain_route(
902                data_route,
903                ChannelId::from_link_port(crate::channel::DEFAULT_LINK_ID, RadioPort::TunnelRx),
904                0,
905                1,
906                1,
907            )
908            .unwrap();
909        let batch = runtime
910            .push_decrypted_fragment(
911                data_runtime,
912                0,
913                &plain(b"data bytes"),
914                &ReceiverBatchOptions {
915                    raw_payload_routes: vec![data_route],
916                    ..ReceiverBatchOptions::default()
917                },
918            )
919            .unwrap();
920
921        assert_eq!(batch.counters.wfb_payloads, 0);
922        assert_eq!(batch.counters.rtp_packets, 0);
923        assert_eq!(batch.counters.raw_payload_count, 1);
924        assert_eq!(batch.raw_payloads[0].data, b"data bytes");
925    }
926
927    #[test]
928    fn direct_payload_runtime_uses_same_video_route_and_rtp_depacketizer() {
929        let video_route = PayloadRouteId::new(1);
930        let mut runtime = ReceiverRuntime::with_direct_video_route(
931            FrameLayout::WithFcs,
932            video_route,
933            ChannelId::default_video(),
934            0,
935        );
936
937        let packet = h264_stap_a_rtp();
938        let batch = runtime
939            .push_direct_payload(
940                runtime.video_runtime(),
941                123,
942                &packet,
943                &ReceiverBatchOptions {
944                    raw_payload_routes: vec![video_route],
945                    ..ReceiverBatchOptions::default()
946                },
947            )
948            .unwrap();
949
950        assert_eq!(batch.counters.wfb_payloads, 1);
951        assert_eq!(batch.counters.rtp_packets, 1);
952        assert_eq!(batch.counters.video_frames, 1);
953        assert_eq!(batch.frames.len(), 1);
954        assert_eq!(batch.frames[0].codec, crate::rtp::Codec::H264);
955        assert!(batch.frames[0].is_keyframe);
956        assert_eq!(batch.raw_payloads[0].data, packet);
957        assert_eq!(batch.fec_counters, FecCounters::default());
958    }
959
960    #[test]
961    fn video_depacketization_can_be_delegated_without_losing_raw_rtp() {
962        let video_route = PayloadRouteId::new(1);
963        let mut runtime = ReceiverRuntime::with_direct_video_route(
964            FrameLayout::WithFcs,
965            video_route,
966            ChannelId::default_video(),
967            0,
968        );
969        let packet = h264_stap_a_rtp();
970
971        let batch = runtime
972            .push_direct_payload(
973                runtime.video_runtime(),
974                123,
975                &packet,
976                &ReceiverBatchOptions {
977                    raw_payload_routes: vec![video_route],
978                    depacketize_video: false,
979                    ..ReceiverBatchOptions::default()
980                },
981            )
982            .unwrap();
983
984        assert_eq!(batch.counters.wfb_payloads, 1);
985        assert_eq!(batch.counters.rtp_packets, 1);
986        assert_eq!(batch.counters.video_frames, 0);
987        assert!(batch.frames.is_empty());
988        assert_eq!(batch.raw_payloads[0].data, packet);
989        assert_eq!(batch.rtp_status, RtpDepacketizerStatus::default());
990    }
991
992    #[test]
993    fn rx_transfer_accepts_explicit_jaguar3_descriptor_layout() {
994        let mut runtime = ReceiverRuntime::with_plain_video_route(
995            FrameLayout::WithFcs,
996            PayloadRouteId::new(1),
997            ChannelId::default_video(),
998            0,
999            1,
1000            1,
1001        )
1002        .unwrap();
1003        let mut transfer = vec![0u8; 32];
1004        transfer[..4].copy_from_slice(&8u32.to_le_bytes());
1005        transfer[24..32].copy_from_slice(&[0x08, 0, 0, 0, 0, 0, 0, 0]);
1006
1007        let batch = runtime
1008            .push_rx_transfer_with_kind(
1009                &transfer,
1010                RxDescriptorKind::Jaguar3,
1011                &ReceiverBatchOptions::default(),
1012            )
1013            .unwrap();
1014
1015        assert_eq!(batch.counters.packets, 1);
1016        assert_eq!(batch.counters.accepted_packets, 1);
1017        assert_eq!(batch.counters.ignored_frames, 1);
1018    }
1019}