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(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 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 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 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 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 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 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 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 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}