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::{rtp_status_object, 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 = setRtpReorderEnabled)]
110    /// Enable or disable the small RTP sequence reorder buffer.
111    pub fn set_rtp_reorder_enabled(&mut self, enabled: bool) {
112        self.runtime.set_rtp_reorder_enabled(enabled);
113    }
114
115    #[wasm_bindgen(js_name = withKeypairAndMavlinkChannel)]
116    /// Create an encrypted WFB receiver with an explicit raw telemetry channel.
117    ///
118    /// This is the historical JS name. New applications should call
119    /// `withKeypairAndTelemetryChannel`.
120    pub fn with_keypair_and_mavlink_channel(
121        channel_id: u32,
122        mavlink_channel_id: u32,
123        keypair: &[u8],
124        minimum_epoch: u64,
125    ) -> Result<OpenIpcReceiver, JsValue> {
126        Self::with_keypair_and_telemetry_channel(
127            channel_id,
128            mavlink_channel_id,
129            keypair,
130            minimum_epoch,
131        )
132    }
133
134    #[wasm_bindgen(js_name = withKeypairAndTelemetryChannel)]
135    /// Create an encrypted WFB receiver with an explicit raw telemetry channel.
136    pub fn with_keypair_and_telemetry_channel(
137        channel_id: u32,
138        telemetry_channel_id: u32,
139        keypair: &[u8],
140        minimum_epoch: u64,
141    ) -> Result<OpenIpcReceiver, JsValue> {
142        let keypair = WfbKeypair::from_bytes(keypair)
143            .map_err(|err| JsValue::from_str(&format!("invalid WFB keypair: {err}")))?;
144        openipc_receiver_with_keypair_and_telemetry_channel_inner(
145            channel_id,
146            telemetry_channel_id,
147            keypair,
148            minimum_epoch,
149        )
150    }
151
152    #[wasm_bindgen(js_name = pushRtpPacket)]
153    /// Push one raw RTP packet and return Annex-B bytes when a frame completes.
154    pub fn push_rtp_packet(&mut self, data: &[u8]) -> Option<Uint8Array> {
155        self.runtime
156            .push_rtp_packet(data)
157            .ok()
158            .and_then(|mut frames| frames.drain(..).next())
159            .map(|frame| Uint8Array::from(frame.data.as_slice()))
160    }
161
162    #[wasm_bindgen(
163        js_name = pushRtpPacketDetailed,
164        unchecked_return_type = "OpenIpcVideoFrame | null"
165    )]
166    /// Push one RTP packet and return a typed frame object when one completes.
167    pub fn push_rtp_packet_detailed(&mut self, data: &[u8]) -> Result<JsValue, JsValue> {
168        match self
169            .runtime
170            .push_rtp_packet(data)
171            .ok()
172            .and_then(|mut frames| frames.drain(..).next())
173        {
174            Some(frame) => Ok(video_frame_object(frame)?.into()),
175            None => Ok(JsValue::NULL),
176        }
177    }
178
179    #[wasm_bindgen(js_name = pushDecryptedFragment)]
180    /// Push an already-decrypted WFB fragment into the video runtime.
181    pub fn push_decrypted_fragment(
182        &mut self,
183        data_nonce_hex: &str,
184        fragment: &[u8],
185    ) -> Result<Array, JsValue> {
186        let data_nonce = parse_hex_u64(data_nonce_hex)?;
187        let batch = self
188            .runtime
189            .push_decrypted_fragment(
190                self.runtime.video_runtime(),
191                data_nonce,
192                fragment,
193                &ReceiverBatchOptions::default(),
194            )
195            .map_err(|err| JsValue::from_str(&format!("WFB fragment rejected: {err}")))?;
196        Ok(frame_bytes_array(batch.frames))
197    }
198
199    #[wasm_bindgen(js_name = pushDecrypted80211Frame)]
200    /// Push an 802.11 frame with a caller-supplied decrypted WFB fragment.
201    pub fn push_decrypted_80211_frame(
202        &mut self,
203        frame: &[u8],
204        fragment: &[u8],
205    ) -> Result<Array, JsValue> {
206        let batch = self
207            .runtime
208            .push_decrypted_80211_frame(
209                self.runtime.video_runtime(),
210                frame,
211                fragment,
212                &ReceiverBatchOptions::default(),
213            )
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 = pushEncrypted80211Frame)]
219    /// Push one encrypted OpenIPC/WFB 802.11 frame.
220    pub fn push_encrypted_80211_frame(&mut self, frame: &[u8]) -> Result<Array, JsValue> {
221        let batch = self
222            .runtime
223            .push_80211_frame(frame, &ReceiverBatchOptions::default())
224            .map_err(|err| JsValue::from_str(&format!("802.11 frame rejected: {err}")))?;
225        Ok(frame_bytes_array(batch.frames))
226    }
227
228    #[wasm_bindgen(js_name = pushRxTransfer)]
229    /// Push one Realtek RX USB transfer and return completed Annex-B frames.
230    pub fn push_rx_transfer(&mut self, transfer: &[u8]) -> Result<Array, JsValue> {
231        let batch = self
232            .runtime
233            .push_rx_transfer(transfer, &ReceiverBatchOptions::default())
234            .map_err(|err| JsValue::from_str(&format!("Realtek RX aggregate rejected: {err}")))?;
235        Ok(frame_bytes_array(batch.frames))
236    }
237
238    #[wasm_bindgen(
239        js_name = pushRxTransferDetailed,
240        unchecked_return_type = "OpenIpcVideoFrame[]"
241    )]
242    /// Push one RX transfer and return typed frame objects.
243    pub fn push_rx_transfer_detailed(&mut self, transfer: &[u8]) -> Result<Array, JsValue> {
244        self.push_rx_transfer_detailed_with_options(transfer, false)
245    }
246
247    #[wasm_bindgen(
248        js_name = pushRxTransferDetailedWithOptions,
249        unchecked_return_type = "OpenIpcVideoFrame[]"
250    )]
251    /// Push one RX transfer with control over CRC/ICV-marked packets.
252    pub fn push_rx_transfer_detailed_with_options(
253        &mut self,
254        transfer: &[u8],
255        keep_corrupted: bool,
256    ) -> Result<Array, JsValue> {
257        let batch = self
258            .runtime
259            .push_rx_transfer(
260                transfer,
261                &ReceiverBatchOptions {
262                    accept_corrupted: keep_corrupted,
263                    ..ReceiverBatchOptions::default()
264                },
265            )
266            .map_err(|err| JsValue::from_str(&format!("Realtek RX aggregate rejected: {err}")))?;
267        frame_objects_array(batch.frames)
268    }
269
270    #[wasm_bindgen(
271        js_name = pushRxTransferProfiled,
272        unchecked_return_type = "OpenIpcRxTransferProfile"
273    )]
274    /// Push one RX transfer and return frames plus parser/latency counters.
275    pub fn push_rx_transfer_profiled(&mut self, transfer: &[u8]) -> Result<Object, JsValue> {
276        self.push_rx_transfer_profiled_with_options(transfer, false)
277    }
278
279    #[wasm_bindgen(
280        js_name = pushRxTransferProfiledWithOptions,
281        unchecked_return_type = "OpenIpcRxTransferProfile"
282    )]
283    /// Push one RX transfer with profiling and bad-FCS handling options.
284    pub fn push_rx_transfer_profiled_with_options(
285        &mut self,
286        transfer: &[u8],
287        keep_corrupted: bool,
288    ) -> Result<Object, JsValue> {
289        self.push_rx_transfer_profiled_inner(
290            transfer,
291            keep_corrupted,
292            &[TELEMETRY_ROUTE_ID.raw() as u32],
293            &[],
294        )
295    }
296
297    #[wasm_bindgen(
298        js_name = pushRxTransferProfiledWithRouteIds,
299        unchecked_return_type = "OpenIpcRxTransferProfile"
300    )]
301    /// Push one RX transfer and copy raw payloads for caller-selected route IDs.
302    pub fn push_rx_transfer_profiled_with_route_ids(
303        &mut self,
304        transfer: &[u8],
305        keep_corrupted: bool,
306        raw_route_ids: &[u32],
307    ) -> Result<Object, JsValue> {
308        self.push_rx_transfer_profiled_inner(transfer, keep_corrupted, raw_route_ids, &[])
309    }
310
311    #[wasm_bindgen(
312        js_name = pushRxTransferProfiledWithRouteIdsAndRtpTaps,
313        unchecked_return_type = "OpenIpcRxTransferProfile"
314    )]
315    /// Push one RX transfer and copy raw payloads plus filtered RTP payload taps.
316    pub fn push_rx_transfer_profiled_with_route_ids_and_rtp_taps(
317        &mut self,
318        transfer: &[u8],
319        keep_corrupted: bool,
320        raw_route_ids: &[u32],
321        rtp_tap_route_ids: &[u32],
322        rtp_tap_payload_types: &[u8],
323    ) -> Result<Object, JsValue> {
324        if rtp_tap_route_ids.len() != rtp_tap_payload_types.len() {
325            return Err(JsValue::from_str(
326                "RTP tap route id and payload type arrays must have the same length",
327            ));
328        }
329        let rtp_payload_taps = rtp_tap_route_ids
330            .iter()
331            .zip(rtp_tap_payload_types.iter())
332            .map(|(route_id, payload_type)| RtpPayloadTap {
333                route_id: PayloadRouteId::new(*route_id as u64),
334                payload_type: *payload_type,
335            })
336            .collect::<Vec<_>>();
337        self.push_rx_transfer_profiled_inner(
338            transfer,
339            keep_corrupted,
340            raw_route_ids,
341            &rtp_payload_taps,
342        )
343    }
344
345    #[wasm_bindgen(js_name = addKeyedRoute)]
346    /// Add an encrypted raw-payload route to the receiver.
347    pub fn add_keyed_route(
348        &mut self,
349        route_id: u32,
350        channel_id: u32,
351        keypair: &[u8],
352        minimum_epoch: u64,
353    ) -> Result<(), JsValue> {
354        let keypair = WfbKeypair::from_bytes(keypair)
355            .map_err(|err| JsValue::from_str(&format!("invalid WFB keypair: {err}")))?;
356        self.runtime
357            .add_keyed_route(
358                PayloadRouteId::new(route_id as u64),
359                ChannelId::new(channel_id),
360                DEFAULT_KEY_SLOT,
361                keypair,
362                minimum_epoch,
363            )
364            .map_err(|err| JsValue::from_str(&format!("invalid route config: {err}")))?;
365        Ok(())
366    }
367
368    #[wasm_bindgen(js_name = fecCounters)]
369    /// Return cumulative video FEC counters as JSON.
370    pub fn fec_counters(&self) -> String {
371        counters_json(self.video_fec_counters())
372    }
373}
374
375impl OpenIpcReceiver {
376    fn push_rx_transfer_profiled_inner(
377        &mut self,
378        transfer: &[u8],
379        keep_corrupted: bool,
380        raw_route_ids: &[u32],
381        rtp_payload_taps: &[RtpPayloadTap],
382    ) -> Result<Object, JsValue> {
383        let total_start = now_ms();
384        let parse_start = now_ms();
385        let packets = parse_rx_aggregate_with_kind(transfer, self.rx_descriptor_kind)
386            .map_err(|err| JsValue::from_str(&format!("Realtek RX aggregate rejected: {err}")))?;
387        let parse_ms = elapsed_ms(parse_start);
388
389        let raw_payload_routes = raw_route_ids
390            .iter()
391            .map(|id| PayloadRouteId::new(*id as u64))
392            .collect();
393        let pipeline_start = now_ms();
394        let batch = self.runtime.push_rx_packets(
395            packets,
396            &ReceiverBatchOptions {
397                accept_corrupted: keep_corrupted,
398                raw_payload_routes,
399                rtp_payload_taps: rtp_payload_taps.to_vec(),
400            },
401        );
402        let pipeline_ms = elapsed_ms(pipeline_start);
403        let counters = batch.counters;
404        let frames = frame_objects_array(batch.frames)?;
405        let raw_payloads = raw_payload_array(batch.raw_payloads)?;
406        let rtp_status = rtp_status_object(batch.rtp_status, batch.rtp_reorder_status)?;
407
408        let object = Object::new();
409        Reflect::set(&object, &JsValue::from_str("frames"), &frames)?;
410        Reflect::set(&object, &JsValue::from_str("rawPayloads"), &raw_payloads)?;
411        Reflect::set(
412            &object,
413            &JsValue::from_str("mavlinkPayloads"),
414            &raw_payloads,
415        )?;
416        Reflect::set(&object, &JsValue::from_str("rtpStatus"), &rtp_status)?;
417        set_number(
418            &object,
419            "rawPayloadCount",
420            counters.raw_payload_count as f64,
421        )?;
422        set_number(
423            &object,
424            "rawPayloadBytes",
425            counters.raw_payload_bytes as f64,
426        )?;
427        set_number(&object, "transferBytes", transfer.len() as f64)?;
428        set_number(&object, "packets", counters.packets as f64)?;
429        set_number(&object, "acceptedPackets", counters.accepted_packets as f64)?;
430        set_number(&object, "droppedPackets", counters.dropped_packets as f64)?;
431        set_number(&object, "crcDropped", counters.crc_dropped as f64)?;
432        set_number(&object, "icvDropped", counters.icv_dropped as f64)?;
433        set_number(&object, "reportDropped", counters.report_dropped as f64)?;
434        set_number(&object, "ignoredFrames", counters.ignored_frames as f64)?;
435        set_number(&object, "sessions", counters.sessions as f64)?;
436        set_number(&object, "wfbPayloads", counters.wfb_payloads as f64)?;
437        set_number(&object, "rtpPackets", counters.rtp_packets as f64)?;
438        set_number(&object, "videoFrames", counters.video_frames as f64)?;
439        set_number(
440            &object,
441            "mavlinkPayloadCount",
442            counters.raw_payload_count as f64,
443        )?;
444        set_number(&object, "mavlinkBytes", counters.raw_payload_bytes as f64)?;
445        set_number(&object, "parseMs", parse_ms)?;
446        set_number(&object, "pipelineMs", pipeline_ms)?;
447        set_number(&object, "totalMs", elapsed_ms(total_start))?;
448        Ok(object)
449    }
450}
451
452fn openipc_receiver_with_keypair_and_telemetry_channel_inner(
453    channel_id: u32,
454    telemetry_channel_id: u32,
455    keypair: WfbKeypair,
456    minimum_epoch: u64,
457) -> Result<OpenIpcReceiver, JsValue> {
458    let mut runtime = ReceiverRuntime::with_keyed_video_route(
459        FrameLayout::WithFcs,
460        VIDEO_ROUTE_ID,
461        ChannelId::new(channel_id),
462        DEFAULT_KEY_SLOT,
463        keypair,
464        minimum_epoch,
465    )
466    .map_err(|err| JsValue::from_str(&format!("invalid encrypted receiver config: {err}")))?;
467    runtime
468        .add_keyed_route(
469            TELEMETRY_ROUTE_ID,
470            ChannelId::new(telemetry_channel_id),
471            DEFAULT_KEY_SLOT,
472            keypair,
473            minimum_epoch,
474        )
475        .map_err(|err| JsValue::from_str(&format!("invalid MAVLink receiver config: {err}")))?;
476    Ok(OpenIpcReceiver {
477        runtime,
478        rx_descriptor_kind: RxDescriptorKind::Jaguar1,
479    })
480}
481
482pub(crate) fn parse_rx_descriptor_kind(kind: &str) -> Result<RxDescriptorKind, JsValue> {
483    match kind {
484        "jaguar1" | "rtl8812" | "rtl8821" | "rtl8814" => Ok(RxDescriptorKind::Jaguar1),
485        "jaguar3" | "rtl8812cu" | "rtl8822c" | "rtl8822cu" => Ok(RxDescriptorKind::Jaguar3),
486        _ => Err(JsValue::from_str(
487            "unsupported RX descriptor kind; expected jaguar1 or jaguar3",
488        )),
489    }
490}
491
492fn frame_bytes_array(frames: Vec<openipc_core::DepacketizedFrame>) -> Array {
493    let out = Array::new();
494    for frame in frames {
495        out.push(&Uint8Array::from(frame.data.as_slice()));
496    }
497    out
498}
499
500fn frame_objects_array(frames: Vec<openipc_core::DepacketizedFrame>) -> Result<Array, JsValue> {
501    let out = Array::new();
502    for frame in frames {
503        out.push(&video_frame_object(frame)?.into());
504    }
505    Ok(out)
506}
507
508fn raw_payload_array(payloads: Vec<openipc_core::RoutePayload>) -> Result<Array, JsValue> {
509    let out = Array::new();
510    for payload in payloads {
511        out.push(&raw_payload_object(payload)?.into());
512    }
513    Ok(out)
514}