Skip to main content

openipc_web/
receiver.rs

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