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_with_kind(
234                transfer,
235                self.rx_descriptor_kind,
236                &ReceiverBatchOptions::default(),
237            )
238            .map_err(|err| JsValue::from_str(&format!("Realtek RX aggregate rejected: {err}")))?;
239        Ok(frame_bytes_array(batch.frames))
240    }
241
242    #[wasm_bindgen(
243        js_name = pushRxTransferDetailed,
244        unchecked_return_type = "OpenIpcVideoFrame[]"
245    )]
246    /// Push one RX transfer and return typed frame objects.
247    pub fn push_rx_transfer_detailed(&mut self, transfer: &[u8]) -> Result<Array, JsValue> {
248        self.push_rx_transfer_detailed_with_options(transfer, false)
249    }
250
251    #[wasm_bindgen(
252        js_name = pushRxTransferDetailedWithOptions,
253        unchecked_return_type = "OpenIpcVideoFrame[]"
254    )]
255    /// Push one RX transfer with control over CRC/ICV-marked packets.
256    pub fn push_rx_transfer_detailed_with_options(
257        &mut self,
258        transfer: &[u8],
259        keep_corrupted: bool,
260    ) -> Result<Array, JsValue> {
261        let batch = self
262            .runtime
263            .push_rx_transfer_with_kind(
264                transfer,
265                self.rx_descriptor_kind,
266                &ReceiverBatchOptions {
267                    accept_corrupted: keep_corrupted,
268                    ..ReceiverBatchOptions::default()
269                },
270            )
271            .map_err(|err| JsValue::from_str(&format!("Realtek RX aggregate rejected: {err}")))?;
272        frame_objects_array(batch.frames)
273    }
274
275    #[wasm_bindgen(
276        js_name = pushRxTransferProfiled,
277        unchecked_return_type = "OpenIpcRxTransferProfile"
278    )]
279    /// Push one RX transfer and return frames plus parser/latency counters.
280    pub fn push_rx_transfer_profiled(&mut self, transfer: &[u8]) -> Result<Object, JsValue> {
281        self.push_rx_transfer_profiled_with_options(transfer, false)
282    }
283
284    #[wasm_bindgen(
285        js_name = pushRxTransferProfiledWithOptions,
286        unchecked_return_type = "OpenIpcRxTransferProfile"
287    )]
288    /// Push one RX transfer with profiling and bad-FCS handling options.
289    pub fn push_rx_transfer_profiled_with_options(
290        &mut self,
291        transfer: &[u8],
292        keep_corrupted: bool,
293    ) -> Result<Object, JsValue> {
294        self.push_rx_transfer_profiled_inner(
295            transfer,
296            keep_corrupted,
297            &[TELEMETRY_ROUTE_ID.raw() as u32],
298            &[],
299        )
300    }
301
302    #[wasm_bindgen(
303        js_name = pushRxTransferProfiledWithRouteIds,
304        unchecked_return_type = "OpenIpcRxTransferProfile"
305    )]
306    /// Push one RX transfer and copy raw payloads for caller-selected route IDs.
307    pub fn push_rx_transfer_profiled_with_route_ids(
308        &mut self,
309        transfer: &[u8],
310        keep_corrupted: bool,
311        raw_route_ids: &[u32],
312    ) -> Result<Object, JsValue> {
313        self.push_rx_transfer_profiled_inner(transfer, keep_corrupted, raw_route_ids, &[])
314    }
315
316    #[wasm_bindgen(
317        js_name = pushRxTransferProfiledWithRouteIdsAndRtpTaps,
318        unchecked_return_type = "OpenIpcRxTransferProfile"
319    )]
320    /// Push one RX transfer and copy raw payloads plus filtered RTP payload taps.
321    pub fn push_rx_transfer_profiled_with_route_ids_and_rtp_taps(
322        &mut self,
323        transfer: &[u8],
324        keep_corrupted: bool,
325        raw_route_ids: &[u32],
326        rtp_tap_route_ids: &[u32],
327        rtp_tap_payload_types: &[u8],
328    ) -> Result<Object, JsValue> {
329        if rtp_tap_route_ids.len() != rtp_tap_payload_types.len() {
330            return Err(JsValue::from_str(
331                "RTP tap route id and payload type arrays must have the same length",
332            ));
333        }
334        let rtp_payload_taps = rtp_tap_route_ids
335            .iter()
336            .zip(rtp_tap_payload_types.iter())
337            .map(|(route_id, payload_type)| RtpPayloadTap {
338                route_id: PayloadRouteId::new(*route_id as u64),
339                payload_type: *payload_type,
340            })
341            .collect::<Vec<_>>();
342        self.push_rx_transfer_profiled_inner(
343            transfer,
344            keep_corrupted,
345            raw_route_ids,
346            &rtp_payload_taps,
347        )
348    }
349
350    #[wasm_bindgen(js_name = addKeyedRoute)]
351    /// Add an encrypted raw-payload route to the receiver.
352    pub fn add_keyed_route(
353        &mut self,
354        route_id: u32,
355        channel_id: u32,
356        keypair: &[u8],
357        minimum_epoch: u64,
358    ) -> Result<(), JsValue> {
359        let keypair = WfbKeypair::from_bytes(keypair)
360            .map_err(|err| JsValue::from_str(&format!("invalid WFB keypair: {err}")))?;
361        self.runtime
362            .add_keyed_route(
363                PayloadRouteId::new(route_id as u64),
364                ChannelId::new(channel_id),
365                DEFAULT_KEY_SLOT,
366                keypair,
367                minimum_epoch,
368            )
369            .map_err(|err| JsValue::from_str(&format!("invalid route config: {err}")))?;
370        Ok(())
371    }
372
373    #[wasm_bindgen(js_name = fecCounters)]
374    /// Return cumulative video FEC counters as JSON.
375    pub fn fec_counters(&self) -> String {
376        counters_json(self.video_fec_counters())
377    }
378}
379
380impl OpenIpcReceiver {
381    fn push_rx_transfer_profiled_inner(
382        &mut self,
383        transfer: &[u8],
384        keep_corrupted: bool,
385        raw_route_ids: &[u32],
386        rtp_payload_taps: &[RtpPayloadTap],
387    ) -> Result<Object, JsValue> {
388        let total_start = now_ms();
389        let parse_start = now_ms();
390        let packets = parse_rx_aggregate_with_kind(transfer, self.rx_descriptor_kind)
391            .map_err(|err| JsValue::from_str(&format!("Realtek RX aggregate rejected: {err}")))?;
392        let parse_ms = elapsed_ms(parse_start);
393
394        let raw_payload_routes = raw_route_ids
395            .iter()
396            .map(|id| PayloadRouteId::new(*id as u64))
397            .collect();
398        let pipeline_start = now_ms();
399        let batch = self.runtime.push_rx_packets(
400            packets,
401            &ReceiverBatchOptions {
402                accept_corrupted: keep_corrupted,
403                raw_payload_routes,
404                rtp_payload_taps: rtp_payload_taps.to_vec(),
405            },
406        );
407        let pipeline_ms = elapsed_ms(pipeline_start);
408        let counters = batch.counters;
409        let frames = frame_objects_array(batch.frames)?;
410        let raw_payloads = raw_payload_array(batch.raw_payloads)?;
411        let rtp_status = rtp_status_object(batch.rtp_status, batch.rtp_reorder_status)?;
412
413        let object = Object::new();
414        Reflect::set(&object, &JsValue::from_str("frames"), &frames)?;
415        Reflect::set(&object, &JsValue::from_str("rawPayloads"), &raw_payloads)?;
416        Reflect::set(
417            &object,
418            &JsValue::from_str("mavlinkPayloads"),
419            &raw_payloads,
420        )?;
421        Reflect::set(&object, &JsValue::from_str("rtpStatus"), &rtp_status)?;
422        set_number(
423            &object,
424            "rawPayloadCount",
425            counters.raw_payload_count as f64,
426        )?;
427        set_number(
428            &object,
429            "rawPayloadBytes",
430            counters.raw_payload_bytes as f64,
431        )?;
432        set_number(&object, "transferBytes", transfer.len() as f64)?;
433        set_number(&object, "packets", counters.packets as f64)?;
434        set_number(&object, "acceptedPackets", counters.accepted_packets as f64)?;
435        set_number(&object, "droppedPackets", counters.dropped_packets as f64)?;
436        set_number(&object, "crcDropped", counters.crc_dropped as f64)?;
437        set_number(&object, "icvDropped", counters.icv_dropped as f64)?;
438        set_number(&object, "reportDropped", counters.report_dropped as f64)?;
439        set_number(&object, "ignoredFrames", counters.ignored_frames as f64)?;
440        set_number(&object, "sessions", counters.sessions as f64)?;
441        set_number(&object, "wfbPayloads", counters.wfb_payloads as f64)?;
442        set_number(&object, "rtpPackets", counters.rtp_packets as f64)?;
443        set_number(&object, "videoFrames", counters.video_frames as f64)?;
444        set_number(
445            &object,
446            "mavlinkPayloadCount",
447            counters.raw_payload_count as f64,
448        )?;
449        set_number(&object, "mavlinkBytes", counters.raw_payload_bytes as f64)?;
450        set_number(&object, "parseMs", parse_ms)?;
451        set_number(&object, "pipelineMs", pipeline_ms)?;
452        set_number(&object, "totalMs", elapsed_ms(total_start))?;
453        Ok(object)
454    }
455}
456
457fn openipc_receiver_with_keypair_and_telemetry_channel_inner(
458    channel_id: u32,
459    telemetry_channel_id: u32,
460    keypair: WfbKeypair,
461    minimum_epoch: u64,
462) -> Result<OpenIpcReceiver, JsValue> {
463    let mut runtime = ReceiverRuntime::with_keyed_video_route(
464        FrameLayout::WithFcs,
465        VIDEO_ROUTE_ID,
466        ChannelId::new(channel_id),
467        DEFAULT_KEY_SLOT,
468        keypair,
469        minimum_epoch,
470    )
471    .map_err(|err| JsValue::from_str(&format!("invalid encrypted receiver config: {err}")))?;
472    runtime
473        .add_keyed_route(
474            TELEMETRY_ROUTE_ID,
475            ChannelId::new(telemetry_channel_id),
476            DEFAULT_KEY_SLOT,
477            keypair,
478            minimum_epoch,
479        )
480        .map_err(|err| JsValue::from_str(&format!("invalid MAVLink receiver config: {err}")))?;
481    Ok(OpenIpcReceiver {
482        runtime,
483        rx_descriptor_kind: RxDescriptorKind::Jaguar1,
484    })
485}
486
487pub(crate) fn parse_rx_descriptor_kind(kind: &str) -> Result<RxDescriptorKind, JsValue> {
488    match kind {
489        "jaguar1" | "rtl8812" | "rtl8821" | "rtl8814" => Ok(RxDescriptorKind::Jaguar1),
490        "jaguar3" | "rtl8812cu" | "rtl8812eu" | "rtl8822c" | "rtl8822cu" | "rtl8822e"
491        | "rtl8822eu" => Ok(RxDescriptorKind::Jaguar3),
492        _ => Err(JsValue::from_str(
493            "unsupported RX descriptor kind; expected jaguar1 or jaguar3",
494        )),
495    }
496}
497
498fn frame_bytes_array(frames: Vec<openipc_core::DepacketizedFrame>) -> Array {
499    let out = Array::new();
500    for frame in frames {
501        out.push(&Uint8Array::from(frame.data.as_slice()));
502    }
503    out
504}
505
506fn frame_objects_array(frames: Vec<openipc_core::DepacketizedFrame>) -> Result<Array, JsValue> {
507    let out = Array::new();
508    for frame in frames {
509        out.push(&video_frame_object(frame)?.into());
510    }
511    Ok(out)
512}
513
514fn raw_payload_array(payloads: Vec<openipc_core::RoutePayload>) -> Result<Array, JsValue> {
515    let out = Array::new();
516    for payload in payloads {
517        out.push(&raw_payload_object(payload)?.into());
518    }
519    Ok(out)
520}