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]
17pub 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}