Skip to main content

openipc_web/
receiver.rs

1use js_sys::{Array, Object, Reflect, Uint8Array};
2use openipc_core::realtek::{parse_rx_aggregate_with_kind, RxDescriptorKind};
3use openipc_core::{
4    ChannelId, FrameLayout, PayloadRouteId, RadioPort, ReceiverBatchOptions, ReceiverRuntime,
5    RtpPayloadTap, WfbKeypair,
6};
7use wasm_bindgen::prelude::*;
8
9use crate::js::{counters_json, elapsed_ms, now_ms, parse_hex_u64, raw_payload_object, set_number};
10use crate::video::video_frame_object;
11
12const VIDEO_ROUTE_ID: PayloadRouteId = PayloadRouteId::new(1);
13const TELEMETRY_ROUTE_ID: PayloadRouteId = PayloadRouteId::new(2);
14const DEFAULT_KEY_SLOT: u64 = 0;
15
16#[wasm_bindgen]
17/// Browser/WASM receiver for OpenIPC RX transfers and RTP packets.
18pub struct OpenIpcReceiver {
19    pub(crate) runtime: ReceiverRuntime,
20    rx_descriptor_kind: RxDescriptorKind,
21}
22
23impl OpenIpcReceiver {
24    pub(crate) fn video_fec_counters(&self) -> openipc_core::FecCounters {
25        self.runtime.video_fec_counters()
26    }
27}
28
29#[wasm_bindgen]
30impl OpenIpcReceiver {
31    #[wasm_bindgen(constructor)]
32    /// Create a plain/FEC-only receiver for the default OpenIPC video channel.
33    pub fn new() -> Result<OpenIpcReceiver, JsValue> {
34        Self::with_channel_id(openipc_core::channel::DEFAULT_LINK_ID << 8, 1, 5)
35    }
36
37    #[wasm_bindgen(js_name = withChannelId)]
38    /// Create a plain/FEC-only receiver for a specific channel id.
39    pub fn with_channel_id(
40        channel_id: u32,
41        fec_k: usize,
42        fec_n: usize,
43    ) -> Result<OpenIpcReceiver, JsValue> {
44        let runtime = ReceiverRuntime::with_plain_video_route(
45            FrameLayout::WithFcs,
46            VIDEO_ROUTE_ID,
47            ChannelId::new(channel_id),
48            DEFAULT_KEY_SLOT,
49            fec_k,
50            fec_n,
51        )
52        .map_err(|err| JsValue::from_str(&format!("invalid receiver config: {err}")))?;
53        Ok(Self {
54            runtime,
55            rx_descriptor_kind: RxDescriptorKind::Jaguar1,
56        })
57    }
58
59    #[wasm_bindgen(js_name = withKeypair)]
60    /// Create an encrypted WFB receiver and default telemetry downlink tap.
61    pub fn with_keypair(
62        channel_id: u32,
63        keypair: &[u8],
64        minimum_epoch: u64,
65    ) -> Result<OpenIpcReceiver, JsValue> {
66        let keypair = WfbKeypair::from_bytes(keypair)
67            .map_err(|err| JsValue::from_str(&format!("invalid WFB keypair: {err}")))?;
68        let telemetry_channel_id =
69            ChannelId::from_link_port(channel_id >> 8, RadioPort::TelemetryRx).raw();
70        openipc_receiver_with_keypair_and_telemetry_channel_inner(
71            channel_id,
72            telemetry_channel_id,
73            keypair,
74            minimum_epoch,
75        )
76    }
77
78    #[wasm_bindgen(js_name = withKeypairOnly)]
79    /// Create an encrypted WFB receiver with only the video route.
80    pub fn with_keypair_only(
81        channel_id: u32,
82        keypair: &[u8],
83        minimum_epoch: u64,
84    ) -> Result<OpenIpcReceiver, JsValue> {
85        let keypair = WfbKeypair::from_bytes(keypair)
86            .map_err(|err| JsValue::from_str(&format!("invalid WFB keypair: {err}")))?;
87        let runtime = ReceiverRuntime::with_keyed_video_route(
88            FrameLayout::WithFcs,
89            VIDEO_ROUTE_ID,
90            ChannelId::new(channel_id),
91            DEFAULT_KEY_SLOT,
92            keypair,
93            minimum_epoch,
94        )
95        .map_err(|err| JsValue::from_str(&format!("invalid encrypted receiver config: {err}")))?;
96        Ok(OpenIpcReceiver {
97            runtime,
98            rx_descriptor_kind: RxDescriptorKind::Jaguar1,
99        })
100    }
101
102    #[wasm_bindgen(js_name = setRxDescriptorKind)]
103    /// Select the Realtek USB RX descriptor layout for future bulk-IN transfers.
104    pub fn set_rx_descriptor_kind(&mut self, kind: &str) -> Result<(), JsValue> {
105        self.rx_descriptor_kind = parse_rx_descriptor_kind(kind)?;
106        Ok(())
107    }
108
109    #[wasm_bindgen(js_name = withKeypairAndMavlinkChannel)]
110    /// Create an encrypted WFB receiver with an explicit raw telemetry channel.
111    ///
112    /// This is the historical JS name. New applications should call
113    /// `withKeypairAndTelemetryChannel`.
114    pub fn with_keypair_and_mavlink_channel(
115        channel_id: u32,
116        mavlink_channel_id: u32,
117        keypair: &[u8],
118        minimum_epoch: u64,
119    ) -> Result<OpenIpcReceiver, JsValue> {
120        Self::with_keypair_and_telemetry_channel(
121            channel_id,
122            mavlink_channel_id,
123            keypair,
124            minimum_epoch,
125        )
126    }
127
128    #[wasm_bindgen(js_name = withKeypairAndTelemetryChannel)]
129    /// Create an encrypted WFB receiver with an explicit raw telemetry channel.
130    pub fn with_keypair_and_telemetry_channel(
131        channel_id: u32,
132        telemetry_channel_id: u32,
133        keypair: &[u8],
134        minimum_epoch: u64,
135    ) -> Result<OpenIpcReceiver, JsValue> {
136        let keypair = WfbKeypair::from_bytes(keypair)
137            .map_err(|err| JsValue::from_str(&format!("invalid WFB keypair: {err}")))?;
138        openipc_receiver_with_keypair_and_telemetry_channel_inner(
139            channel_id,
140            telemetry_channel_id,
141            keypair,
142            minimum_epoch,
143        )
144    }
145
146    #[wasm_bindgen(js_name = pushRtpPacket)]
147    /// Push one raw RTP packet and return Annex-B bytes when a frame completes.
148    pub fn push_rtp_packet(&mut self, data: &[u8]) -> Option<Uint8Array> {
149        self.runtime
150            .rtp_mut()
151            .push(data)
152            .ok()
153            .flatten()
154            .map(|frame| Uint8Array::from(frame.data.as_slice()))
155    }
156
157    #[wasm_bindgen(
158        js_name = pushRtpPacketDetailed,
159        unchecked_return_type = "OpenIpcVideoFrame | null"
160    )]
161    /// Push one RTP packet and return a typed frame object when one completes.
162    pub fn push_rtp_packet_detailed(&mut self, data: &[u8]) -> Result<JsValue, JsValue> {
163        match self.runtime.rtp_mut().push(data).ok().flatten() {
164            Some(frame) => Ok(video_frame_object(frame)?.into()),
165            None => Ok(JsValue::NULL),
166        }
167    }
168
169    #[wasm_bindgen(js_name = pushDecryptedFragment)]
170    /// Push an already-decrypted WFB fragment into the video runtime.
171    pub fn push_decrypted_fragment(
172        &mut self,
173        data_nonce_hex: &str,
174        fragment: &[u8],
175    ) -> Result<Array, JsValue> {
176        let data_nonce = parse_hex_u64(data_nonce_hex)?;
177        let batch = self
178            .runtime
179            .push_decrypted_fragment(
180                self.runtime.video_runtime(),
181                data_nonce,
182                fragment,
183                &ReceiverBatchOptions::default(),
184            )
185            .map_err(|err| JsValue::from_str(&format!("WFB fragment rejected: {err}")))?;
186        Ok(frame_bytes_array(batch.frames))
187    }
188
189    #[wasm_bindgen(js_name = pushDecrypted80211Frame)]
190    /// Push an 802.11 frame with a caller-supplied decrypted WFB fragment.
191    pub fn push_decrypted_80211_frame(
192        &mut self,
193        frame: &[u8],
194        fragment: &[u8],
195    ) -> Result<Array, JsValue> {
196        let batch = self
197            .runtime
198            .push_decrypted_80211_frame(
199                self.runtime.video_runtime(),
200                frame,
201                fragment,
202                &ReceiverBatchOptions::default(),
203            )
204            .map_err(|err| JsValue::from_str(&format!("802.11 frame rejected: {err}")))?;
205        Ok(frame_bytes_array(batch.frames))
206    }
207
208    #[wasm_bindgen(js_name = pushEncrypted80211Frame)]
209    /// Push one encrypted OpenIPC/WFB 802.11 frame.
210    pub fn push_encrypted_80211_frame(&mut self, frame: &[u8]) -> Result<Array, JsValue> {
211        let batch = self
212            .runtime
213            .push_80211_frame(frame, &ReceiverBatchOptions::default())
214            .map_err(|err| JsValue::from_str(&format!("802.11 frame rejected: {err}")))?;
215        Ok(frame_bytes_array(batch.frames))
216    }
217
218    #[wasm_bindgen(js_name = pushRxTransfer)]
219    /// Push one Realtek RX USB transfer and return completed Annex-B frames.
220    pub fn push_rx_transfer(&mut self, transfer: &[u8]) -> Result<Array, JsValue> {
221        let batch = self
222            .runtime
223            .push_rx_transfer(transfer, &ReceiverBatchOptions::default())
224            .map_err(|err| JsValue::from_str(&format!("Realtek RX aggregate rejected: {err}")))?;
225        Ok(frame_bytes_array(batch.frames))
226    }
227
228    #[wasm_bindgen(
229        js_name = pushRxTransferDetailed,
230        unchecked_return_type = "OpenIpcVideoFrame[]"
231    )]
232    /// Push one RX transfer and return typed frame objects.
233    pub fn push_rx_transfer_detailed(&mut self, transfer: &[u8]) -> Result<Array, JsValue> {
234        self.push_rx_transfer_detailed_with_options(transfer, false)
235    }
236
237    #[wasm_bindgen(
238        js_name = pushRxTransferDetailedWithOptions,
239        unchecked_return_type = "OpenIpcVideoFrame[]"
240    )]
241    /// Push one RX transfer with control over CRC/ICV-marked packets.
242    pub fn push_rx_transfer_detailed_with_options(
243        &mut self,
244        transfer: &[u8],
245        keep_corrupted: bool,
246    ) -> Result<Array, JsValue> {
247        let batch = self
248            .runtime
249            .push_rx_transfer(
250                transfer,
251                &ReceiverBatchOptions {
252                    accept_corrupted: keep_corrupted,
253                    ..ReceiverBatchOptions::default()
254                },
255            )
256            .map_err(|err| JsValue::from_str(&format!("Realtek RX aggregate rejected: {err}")))?;
257        frame_objects_array(batch.frames)
258    }
259
260    #[wasm_bindgen(
261        js_name = pushRxTransferProfiled,
262        unchecked_return_type = "OpenIpcRxTransferProfile"
263    )]
264    /// Push one RX transfer and return frames plus parser/latency counters.
265    pub fn push_rx_transfer_profiled(&mut self, transfer: &[u8]) -> Result<Object, JsValue> {
266        self.push_rx_transfer_profiled_with_options(transfer, false)
267    }
268
269    #[wasm_bindgen(
270        js_name = pushRxTransferProfiledWithOptions,
271        unchecked_return_type = "OpenIpcRxTransferProfile"
272    )]
273    /// Push one RX transfer with profiling and bad-FCS handling options.
274    pub fn push_rx_transfer_profiled_with_options(
275        &mut self,
276        transfer: &[u8],
277        keep_corrupted: bool,
278    ) -> Result<Object, JsValue> {
279        self.push_rx_transfer_profiled_inner(
280            transfer,
281            keep_corrupted,
282            &[TELEMETRY_ROUTE_ID.raw() as u32],
283            &[],
284        )
285    }
286
287    #[wasm_bindgen(
288        js_name = pushRxTransferProfiledWithRouteIds,
289        unchecked_return_type = "OpenIpcRxTransferProfile"
290    )]
291    /// Push one RX transfer and copy raw payloads for caller-selected route IDs.
292    pub fn push_rx_transfer_profiled_with_route_ids(
293        &mut self,
294        transfer: &[u8],
295        keep_corrupted: bool,
296        raw_route_ids: &[u32],
297    ) -> Result<Object, JsValue> {
298        self.push_rx_transfer_profiled_inner(transfer, keep_corrupted, raw_route_ids, &[])
299    }
300
301    #[wasm_bindgen(
302        js_name = pushRxTransferProfiledWithRouteIdsAndRtpTaps,
303        unchecked_return_type = "OpenIpcRxTransferProfile"
304    )]
305    /// Push one RX transfer and copy raw payloads plus filtered RTP payload taps.
306    pub fn push_rx_transfer_profiled_with_route_ids_and_rtp_taps(
307        &mut self,
308        transfer: &[u8],
309        keep_corrupted: bool,
310        raw_route_ids: &[u32],
311        rtp_tap_route_ids: &[u32],
312        rtp_tap_payload_types: &[u8],
313    ) -> Result<Object, JsValue> {
314        if rtp_tap_route_ids.len() != rtp_tap_payload_types.len() {
315            return Err(JsValue::from_str(
316                "RTP tap route id and payload type arrays must have the same length",
317            ));
318        }
319        let rtp_payload_taps = rtp_tap_route_ids
320            .iter()
321            .zip(rtp_tap_payload_types.iter())
322            .map(|(route_id, payload_type)| RtpPayloadTap {
323                route_id: PayloadRouteId::new(*route_id as u64),
324                payload_type: *payload_type,
325            })
326            .collect::<Vec<_>>();
327        self.push_rx_transfer_profiled_inner(
328            transfer,
329            keep_corrupted,
330            raw_route_ids,
331            &rtp_payload_taps,
332        )
333    }
334
335    #[wasm_bindgen(js_name = addKeyedRoute)]
336    /// Add an encrypted raw-payload route to the receiver.
337    pub fn add_keyed_route(
338        &mut self,
339        route_id: u32,
340        channel_id: u32,
341        keypair: &[u8],
342        minimum_epoch: u64,
343    ) -> Result<(), JsValue> {
344        let keypair = WfbKeypair::from_bytes(keypair)
345            .map_err(|err| JsValue::from_str(&format!("invalid WFB keypair: {err}")))?;
346        self.runtime
347            .add_keyed_route(
348                PayloadRouteId::new(route_id as u64),
349                ChannelId::new(channel_id),
350                DEFAULT_KEY_SLOT,
351                keypair,
352                minimum_epoch,
353            )
354            .map_err(|err| JsValue::from_str(&format!("invalid route config: {err}")))?;
355        Ok(())
356    }
357
358    #[wasm_bindgen(js_name = fecCounters)]
359    /// Return cumulative video FEC counters as JSON.
360    pub fn fec_counters(&self) -> String {
361        counters_json(self.video_fec_counters())
362    }
363}
364
365impl OpenIpcReceiver {
366    fn push_rx_transfer_profiled_inner(
367        &mut self,
368        transfer: &[u8],
369        keep_corrupted: bool,
370        raw_route_ids: &[u32],
371        rtp_payload_taps: &[RtpPayloadTap],
372    ) -> Result<Object, JsValue> {
373        let total_start = now_ms();
374        let parse_start = now_ms();
375        let packets = parse_rx_aggregate_with_kind(transfer, self.rx_descriptor_kind)
376            .map_err(|err| JsValue::from_str(&format!("Realtek RX aggregate rejected: {err}")))?;
377        let parse_ms = elapsed_ms(parse_start);
378
379        let raw_payload_routes = raw_route_ids
380            .iter()
381            .map(|id| PayloadRouteId::new(*id as u64))
382            .collect();
383        let pipeline_start = now_ms();
384        let batch = self.runtime.push_rx_packets(
385            packets,
386            &ReceiverBatchOptions {
387                accept_corrupted: keep_corrupted,
388                raw_payload_routes,
389                rtp_payload_taps: rtp_payload_taps.to_vec(),
390            },
391        );
392        let pipeline_ms = elapsed_ms(pipeline_start);
393        let counters = batch.counters;
394        let frames = frame_objects_array(batch.frames)?;
395        let raw_payloads = raw_payload_array(batch.raw_payloads)?;
396
397        let object = Object::new();
398        Reflect::set(&object, &JsValue::from_str("frames"), &frames)?;
399        Reflect::set(&object, &JsValue::from_str("rawPayloads"), &raw_payloads)?;
400        Reflect::set(
401            &object,
402            &JsValue::from_str("mavlinkPayloads"),
403            &raw_payloads,
404        )?;
405        set_number(
406            &object,
407            "rawPayloadCount",
408            counters.raw_payload_count as f64,
409        )?;
410        set_number(
411            &object,
412            "rawPayloadBytes",
413            counters.raw_payload_bytes as f64,
414        )?;
415        set_number(&object, "transferBytes", transfer.len() as f64)?;
416        set_number(&object, "packets", counters.packets as f64)?;
417        set_number(&object, "acceptedPackets", counters.accepted_packets as f64)?;
418        set_number(&object, "droppedPackets", counters.dropped_packets as f64)?;
419        set_number(&object, "crcDropped", counters.crc_dropped as f64)?;
420        set_number(&object, "icvDropped", counters.icv_dropped as f64)?;
421        set_number(&object, "reportDropped", counters.report_dropped as f64)?;
422        set_number(&object, "ignoredFrames", counters.ignored_frames as f64)?;
423        set_number(&object, "sessions", counters.sessions as f64)?;
424        set_number(&object, "wfbPayloads", counters.wfb_payloads as f64)?;
425        set_number(&object, "rtpPackets", counters.rtp_packets as f64)?;
426        set_number(&object, "videoFrames", counters.video_frames as f64)?;
427        set_number(
428            &object,
429            "mavlinkPayloadCount",
430            counters.raw_payload_count as f64,
431        )?;
432        set_number(&object, "mavlinkBytes", counters.raw_payload_bytes as f64)?;
433        set_number(&object, "parseMs", parse_ms)?;
434        set_number(&object, "pipelineMs", pipeline_ms)?;
435        set_number(&object, "totalMs", elapsed_ms(total_start))?;
436        Ok(object)
437    }
438}
439
440fn openipc_receiver_with_keypair_and_telemetry_channel_inner(
441    channel_id: u32,
442    telemetry_channel_id: u32,
443    keypair: WfbKeypair,
444    minimum_epoch: u64,
445) -> Result<OpenIpcReceiver, JsValue> {
446    let mut runtime = ReceiverRuntime::with_keyed_video_route(
447        FrameLayout::WithFcs,
448        VIDEO_ROUTE_ID,
449        ChannelId::new(channel_id),
450        DEFAULT_KEY_SLOT,
451        keypair,
452        minimum_epoch,
453    )
454    .map_err(|err| JsValue::from_str(&format!("invalid encrypted receiver config: {err}")))?;
455    runtime
456        .add_keyed_route(
457            TELEMETRY_ROUTE_ID,
458            ChannelId::new(telemetry_channel_id),
459            DEFAULT_KEY_SLOT,
460            keypair,
461            minimum_epoch,
462        )
463        .map_err(|err| JsValue::from_str(&format!("invalid MAVLink receiver config: {err}")))?;
464    Ok(OpenIpcReceiver {
465        runtime,
466        rx_descriptor_kind: RxDescriptorKind::Jaguar1,
467    })
468}
469
470pub(crate) fn parse_rx_descriptor_kind(kind: &str) -> Result<RxDescriptorKind, JsValue> {
471    match kind {
472        "jaguar1" | "rtl8812" | "rtl8821" | "rtl8814" => Ok(RxDescriptorKind::Jaguar1),
473        "jaguar3" | "rtl8812cu" | "rtl8822c" | "rtl8822cu" => Ok(RxDescriptorKind::Jaguar3),
474        _ => Err(JsValue::from_str(
475            "unsupported RX descriptor kind; expected jaguar1 or jaguar3",
476        )),
477    }
478}
479
480fn frame_bytes_array(frames: Vec<openipc_core::DepacketizedFrame>) -> Array {
481    let out = Array::new();
482    for frame in frames {
483        out.push(&Uint8Array::from(frame.data.as_slice()));
484    }
485    out
486}
487
488fn frame_objects_array(frames: Vec<openipc_core::DepacketizedFrame>) -> Result<Array, JsValue> {
489    let out = Array::new();
490    for frame in frames {
491        out.push(&video_frame_object(frame)?.into());
492    }
493    Ok(out)
494}
495
496fn raw_payload_array(payloads: Vec<openipc_core::RoutePayload>) -> Result<Array, JsValue> {
497    let out = Array::new();
498    for payload in payloads {
499        out.push(&raw_payload_object(payload)?.into());
500    }
501    Ok(out)
502}