1use js_sys::{Array, Object, Reflect, Uint8Array};
2use openipc_core::realtek::{parse_rx_aggregate_with_kind, RxDescriptorKind};
3use openipc_core::{
4 ChannelId, DamagedFramePolicy, FrameLayout, PayloadRouteId, RadioPort, ReceiverBatch,
5 ReceiverBatchOptions, ReceiverRuntime, RtpPayloadTap, WfbKeypair,
6};
7use wasm_bindgen::prelude::*;
8
9use crate::js::{
10 counters_json, counters_object, elapsed_ms, now_ms, parse_hex_u64, raw_payload_object,
11 set_number,
12};
13use crate::video::{rtp_status_object, video_frame_object};
14
15const VIDEO_ROUTE_ID: PayloadRouteId = PayloadRouteId::new(1);
16const TELEMETRY_ROUTE_ID: PayloadRouteId = PayloadRouteId::new(2);
17const DEFAULT_KEY_SLOT: u64 = 0;
18
19#[wasm_bindgen]
20pub struct OpenIpcReceiver {
22 pub(crate) runtime: ReceiverRuntime,
23 pub(crate) rx_descriptor_kind: RxDescriptorKind,
24}
25
26impl OpenIpcReceiver {
27 fn from_runtime(mut runtime: ReceiverRuntime) -> Self {
28 runtime
29 .rtp_mut()
30 .set_damaged_frame_policy(DamagedFramePolicy::Forward);
31 Self {
32 runtime,
33 rx_descriptor_kind: RxDescriptorKind::Jaguar1,
34 }
35 }
36
37 pub(crate) fn video_fec_counters(&self) -> openipc_core::FecCounters {
38 self.runtime.video_fec_counters()
39 }
40}
41
42#[wasm_bindgen]
43impl OpenIpcReceiver {
44 #[wasm_bindgen(constructor)]
45 pub fn new() -> Result<OpenIpcReceiver, JsValue> {
47 Self::with_channel_id(openipc_core::channel::DEFAULT_LINK_ID << 8, 1, 5)
48 }
49
50 #[wasm_bindgen(js_name = withChannelId)]
51 pub fn with_channel_id(
53 channel_id: u32,
54 fec_k: usize,
55 fec_n: usize,
56 ) -> Result<OpenIpcReceiver, JsValue> {
57 let runtime = ReceiverRuntime::with_plain_video_route(
58 FrameLayout::WithFcs,
59 VIDEO_ROUTE_ID,
60 ChannelId::new(channel_id),
61 DEFAULT_KEY_SLOT,
62 fec_k,
63 fec_n,
64 )
65 .map_err(|err| JsValue::from_str(&format!("invalid receiver config: {err}")))?;
66 Ok(Self::from_runtime(runtime))
67 }
68
69 #[wasm_bindgen(js_name = withKeypair)]
70 pub fn with_keypair(
72 channel_id: u32,
73 keypair: &[u8],
74 minimum_epoch: u64,
75 ) -> Result<OpenIpcReceiver, JsValue> {
76 let keypair = WfbKeypair::from_bytes(keypair)
77 .map_err(|err| JsValue::from_str(&format!("invalid WFB keypair: {err}")))?;
78 let telemetry_channel_id =
79 ChannelId::from_link_port(channel_id >> 8, RadioPort::TelemetryRx).raw();
80 openipc_receiver_with_keypair_and_telemetry_channel_inner(
81 channel_id,
82 telemetry_channel_id,
83 keypair,
84 minimum_epoch,
85 )
86 }
87
88 #[wasm_bindgen(js_name = withKeypairOnly)]
89 pub fn with_keypair_only(
91 channel_id: u32,
92 keypair: &[u8],
93 minimum_epoch: u64,
94 ) -> Result<OpenIpcReceiver, JsValue> {
95 let keypair = WfbKeypair::from_bytes(keypair)
96 .map_err(|err| JsValue::from_str(&format!("invalid WFB keypair: {err}")))?;
97 let runtime = ReceiverRuntime::with_keyed_video_route(
98 FrameLayout::WithFcs,
99 VIDEO_ROUTE_ID,
100 ChannelId::new(channel_id),
101 DEFAULT_KEY_SLOT,
102 keypair,
103 minimum_epoch,
104 )
105 .map_err(|err| JsValue::from_str(&format!("invalid encrypted receiver config: {err}")))?;
106 Ok(OpenIpcReceiver::from_runtime(runtime))
107 }
108
109 #[wasm_bindgen(js_name = setRxDescriptorKind)]
110 pub fn set_rx_descriptor_kind(&mut self, kind: &str) -> Result<(), JsValue> {
112 self.rx_descriptor_kind = parse_rx_descriptor_kind(kind)?;
113 Ok(())
114 }
115
116 #[wasm_bindgen(js_name = setRtpReorderEnabled)]
117 pub fn set_rtp_reorder_enabled(&mut self, enabled: bool) {
119 self.runtime.set_rtp_reorder_enabled(enabled);
120 }
121
122 #[wasm_bindgen(js_name = setForwardDamagedFrames)]
123 pub fn set_forward_damaged_frames(&mut self, enabled: bool) {
125 self.runtime.rtp_mut().set_damaged_frame_policy(if enabled {
126 DamagedFramePolicy::Forward
127 } else {
128 DamagedFramePolicy::Drop
129 });
130 }
131
132 #[wasm_bindgen(js_name = withKeypairAndMavlinkChannel)]
133 pub fn with_keypair_and_mavlink_channel(
138 channel_id: u32,
139 mavlink_channel_id: u32,
140 keypair: &[u8],
141 minimum_epoch: u64,
142 ) -> Result<OpenIpcReceiver, JsValue> {
143 Self::with_keypair_and_telemetry_channel(
144 channel_id,
145 mavlink_channel_id,
146 keypair,
147 minimum_epoch,
148 )
149 }
150
151 #[wasm_bindgen(js_name = withKeypairAndTelemetryChannel)]
152 pub fn with_keypair_and_telemetry_channel(
154 channel_id: u32,
155 telemetry_channel_id: u32,
156 keypair: &[u8],
157 minimum_epoch: u64,
158 ) -> Result<OpenIpcReceiver, JsValue> {
159 let keypair = WfbKeypair::from_bytes(keypair)
160 .map_err(|err| JsValue::from_str(&format!("invalid WFB keypair: {err}")))?;
161 openipc_receiver_with_keypair_and_telemetry_channel_inner(
162 channel_id,
163 telemetry_channel_id,
164 keypair,
165 minimum_epoch,
166 )
167 }
168
169 #[wasm_bindgen(js_name = pushRtpPacket)]
170 pub fn push_rtp_packet(&mut self, data: &[u8]) -> Option<Uint8Array> {
172 self.runtime
173 .push_rtp_packet(data)
174 .ok()
175 .and_then(|mut frames| frames.drain(..).next())
176 .map(|frame| Uint8Array::from(frame.data.as_slice()))
177 }
178
179 #[wasm_bindgen(
180 js_name = pushRtpPacketDetailed,
181 unchecked_return_type = "OpenIpcVideoFrame | null"
182 )]
183 pub fn push_rtp_packet_detailed(&mut self, data: &[u8]) -> Result<JsValue, JsValue> {
185 match self
186 .runtime
187 .push_rtp_packet(data)
188 .ok()
189 .and_then(|mut frames| frames.drain(..).next())
190 {
191 Some(frame) => Ok(video_frame_object(frame)?.into()),
192 None => Ok(JsValue::NULL),
193 }
194 }
195
196 #[wasm_bindgen(js_name = pushDecryptedFragment)]
197 pub fn push_decrypted_fragment(
199 &mut self,
200 data_nonce_hex: &str,
201 fragment: &[u8],
202 ) -> Result<Array, JsValue> {
203 let data_nonce = parse_hex_u64(data_nonce_hex)?;
204 let batch = self
205 .runtime
206 .push_decrypted_fragment(
207 self.runtime.video_runtime(),
208 data_nonce,
209 fragment,
210 &ReceiverBatchOptions::default(),
211 )
212 .map_err(|err| JsValue::from_str(&format!("WFB fragment rejected: {err}")))?;
213 Ok(frame_bytes_array(batch.frames))
214 }
215
216 #[wasm_bindgen(js_name = pushDecrypted80211Frame)]
217 pub fn push_decrypted_80211_frame(
219 &mut self,
220 frame: &[u8],
221 fragment: &[u8],
222 ) -> Result<Array, JsValue> {
223 let batch = self
224 .runtime
225 .push_decrypted_80211_frame(
226 self.runtime.video_runtime(),
227 frame,
228 fragment,
229 &ReceiverBatchOptions::default(),
230 )
231 .map_err(|err| JsValue::from_str(&format!("802.11 frame rejected: {err}")))?;
232 Ok(frame_bytes_array(batch.frames))
233 }
234
235 #[wasm_bindgen(js_name = pushEncrypted80211Frame)]
236 pub fn push_encrypted_80211_frame(&mut self, frame: &[u8]) -> Result<Array, JsValue> {
238 let batch = self
239 .runtime
240 .push_80211_frame(frame, &ReceiverBatchOptions::default())
241 .map_err(|err| JsValue::from_str(&format!("802.11 frame rejected: {err}")))?;
242 Ok(frame_bytes_array(batch.frames))
243 }
244
245 #[wasm_bindgen(js_name = pushRxTransfer)]
246 pub fn push_rx_transfer(&mut self, transfer: &[u8]) -> Result<Array, JsValue> {
248 let batch = self
249 .runtime
250 .push_rx_transfer_with_kind(
251 transfer,
252 self.rx_descriptor_kind,
253 &ReceiverBatchOptions::default(),
254 )
255 .map_err(|err| JsValue::from_str(&format!("Realtek RX aggregate rejected: {err}")))?;
256 Ok(frame_bytes_array(batch.frames))
257 }
258
259 #[wasm_bindgen(
260 js_name = pushRxTransferDetailed,
261 unchecked_return_type = "OpenIpcVideoFrame[]"
262 )]
263 pub fn push_rx_transfer_detailed(&mut self, transfer: &[u8]) -> Result<Array, JsValue> {
265 self.push_rx_transfer_detailed_with_options(transfer, false)
266 }
267
268 #[wasm_bindgen(
269 js_name = pushRxTransferDetailedWithOptions,
270 unchecked_return_type = "OpenIpcVideoFrame[]"
271 )]
272 pub fn push_rx_transfer_detailed_with_options(
274 &mut self,
275 transfer: &[u8],
276 keep_corrupted: bool,
277 ) -> Result<Array, JsValue> {
278 let batch = self
279 .runtime
280 .push_rx_transfer_with_kind(
281 transfer,
282 self.rx_descriptor_kind,
283 &ReceiverBatchOptions {
284 accept_corrupted: keep_corrupted,
285 ..ReceiverBatchOptions::default()
286 },
287 )
288 .map_err(|err| JsValue::from_str(&format!("Realtek RX aggregate rejected: {err}")))?;
289 frame_objects_array(batch.frames)
290 }
291
292 #[wasm_bindgen(
293 js_name = pushRxTransferProfiled,
294 unchecked_return_type = "OpenIpcRxTransferProfile"
295 )]
296 pub fn push_rx_transfer_profiled(&mut self, transfer: &[u8]) -> Result<Object, JsValue> {
298 self.push_rx_transfer_profiled_with_options(transfer, false)
299 }
300
301 #[wasm_bindgen(
302 js_name = pushRxTransferProfiledWithOptions,
303 unchecked_return_type = "OpenIpcRxTransferProfile"
304 )]
305 pub fn push_rx_transfer_profiled_with_options(
307 &mut self,
308 transfer: &[u8],
309 keep_corrupted: bool,
310 ) -> Result<Object, JsValue> {
311 self.push_rx_transfer_profiled_inner(
312 transfer,
313 keep_corrupted,
314 &[TELEMETRY_ROUTE_ID.raw() as u32],
315 &[],
316 )
317 }
318
319 #[wasm_bindgen(
320 js_name = pushRxTransferProfiledWithRouteIds,
321 unchecked_return_type = "OpenIpcRxTransferProfile"
322 )]
323 pub fn push_rx_transfer_profiled_with_route_ids(
325 &mut self,
326 transfer: &[u8],
327 keep_corrupted: bool,
328 raw_route_ids: &[u32],
329 ) -> Result<Object, JsValue> {
330 self.push_rx_transfer_profiled_inner(transfer, keep_corrupted, raw_route_ids, &[])
331 }
332
333 #[wasm_bindgen(
334 js_name = pushRxTransferProfiledWithRouteIdsAndRtpTaps,
335 unchecked_return_type = "OpenIpcRxTransferProfile"
336 )]
337 pub fn push_rx_transfer_profiled_with_route_ids_and_rtp_taps(
339 &mut self,
340 transfer: &[u8],
341 keep_corrupted: bool,
342 raw_route_ids: &[u32],
343 rtp_tap_route_ids: &[u32],
344 rtp_tap_payload_types: &[u8],
345 ) -> Result<Object, JsValue> {
346 if rtp_tap_route_ids.len() != rtp_tap_payload_types.len() {
347 return Err(JsValue::from_str(
348 "RTP tap route id and payload type arrays must have the same length",
349 ));
350 }
351 let rtp_payload_taps = rtp_tap_route_ids
352 .iter()
353 .zip(rtp_tap_payload_types.iter())
354 .map(|(route_id, payload_type)| RtpPayloadTap {
355 route_id: PayloadRouteId::new(*route_id as u64),
356 payload_type: *payload_type,
357 })
358 .collect::<Vec<_>>();
359 self.push_rx_transfer_profiled_inner(
360 transfer,
361 keep_corrupted,
362 raw_route_ids,
363 &rtp_payload_taps,
364 )
365 }
366
367 #[wasm_bindgen(js_name = addKeyedRoute)]
368 pub fn add_keyed_route(
370 &mut self,
371 route_id: u32,
372 channel_id: u32,
373 keypair: &[u8],
374 minimum_epoch: u64,
375 ) -> Result<(), JsValue> {
376 let keypair = WfbKeypair::from_bytes(keypair)
377 .map_err(|err| JsValue::from_str(&format!("invalid WFB keypair: {err}")))?;
378 self.runtime
379 .add_keyed_route(
380 PayloadRouteId::new(route_id as u64),
381 ChannelId::new(channel_id),
382 DEFAULT_KEY_SLOT,
383 keypair,
384 minimum_epoch,
385 )
386 .map_err(|err| JsValue::from_str(&format!("invalid route config: {err}")))?;
387 Ok(())
388 }
389
390 #[wasm_bindgen(js_name = fecCounters)]
391 pub fn fec_counters(&self) -> String {
393 counters_json(self.video_fec_counters())
394 }
395}
396
397impl OpenIpcReceiver {
398 fn push_rx_transfer_profiled_inner(
399 &mut self,
400 transfer: &[u8],
401 keep_corrupted: bool,
402 raw_route_ids: &[u32],
403 rtp_payload_taps: &[RtpPayloadTap],
404 ) -> Result<Object, JsValue> {
405 let total_start = now_ms();
406 let parse_start = now_ms();
407 let packets = parse_rx_aggregate_with_kind(transfer, self.rx_descriptor_kind)
408 .map_err(|err| JsValue::from_str(&format!("Realtek RX aggregate rejected: {err}")))?;
409 let parse_ms = elapsed_ms(parse_start);
410
411 let raw_payload_routes = raw_route_ids
412 .iter()
413 .map(|id| PayloadRouteId::new(*id as u64))
414 .collect();
415 let pipeline_start = now_ms();
416 let batch = self.runtime.push_rx_packets(
417 packets,
418 &ReceiverBatchOptions {
419 accept_corrupted: keep_corrupted,
420 raw_payload_routes,
421 rtp_payload_taps: rtp_payload_taps.to_vec(),
422 depacketize_video: true,
423 },
424 );
425 let pipeline_ms = elapsed_ms(pipeline_start);
426 receiver_profile_object(
427 batch,
428 transfer.len(),
429 parse_ms,
430 pipeline_ms,
431 elapsed_ms(total_start),
432 )
433 }
434}
435
436pub(crate) fn receiver_profile_object(
437 batch: ReceiverBatch,
438 transfer_bytes: usize,
439 parse_ms: f64,
440 pipeline_ms: f64,
441 total_ms: f64,
442) -> Result<Object, JsValue> {
443 let counters = batch.counters;
444 let fec_counters = batch.fec_counters;
445 let frames = frame_objects_array(batch.frames)?;
446 let raw_payloads = raw_payload_array(batch.raw_payloads)?;
447 let rtp_status = rtp_status_object(batch.rtp_status, batch.rtp_reorder_status)?;
448
449 let object = Object::new();
450 Reflect::set(&object, &JsValue::from_str("frames"), &frames)?;
451 Reflect::set(&object, &JsValue::from_str("rawPayloads"), &raw_payloads)?;
452 Reflect::set(
453 &object,
454 &JsValue::from_str("mavlinkPayloads"),
455 &raw_payloads,
456 )?;
457 Reflect::set(&object, &JsValue::from_str("rtpStatus"), &rtp_status)?;
458 Reflect::set(
459 &object,
460 &JsValue::from_str("fecCounters"),
461 &counters_object(fec_counters)?.into(),
462 )?;
463 set_number(
464 &object,
465 "rawPayloadCount",
466 counters.raw_payload_count as f64,
467 )?;
468 set_number(
469 &object,
470 "rawPayloadBytes",
471 counters.raw_payload_bytes as f64,
472 )?;
473 set_number(&object, "transferBytes", transfer_bytes as f64)?;
474 set_number(&object, "packets", counters.packets as f64)?;
475 set_number(&object, "acceptedPackets", counters.accepted_packets as f64)?;
476 set_number(&object, "droppedPackets", counters.dropped_packets as f64)?;
477 set_number(&object, "crcDropped", counters.crc_dropped as f64)?;
478 set_number(&object, "icvDropped", counters.icv_dropped as f64)?;
479 set_number(&object, "reportDropped", counters.report_dropped as f64)?;
480 set_number(&object, "ignoredFrames", counters.ignored_frames as f64)?;
481 set_number(&object, "sessions", counters.sessions as f64)?;
482 set_number(&object, "wfbPayloads", counters.wfb_payloads as f64)?;
483 set_number(&object, "rtpPackets", counters.rtp_packets as f64)?;
484 set_number(&object, "videoFrames", counters.video_frames as f64)?;
485 set_number(
486 &object,
487 "mavlinkPayloadCount",
488 counters.raw_payload_count as f64,
489 )?;
490 set_number(&object, "mavlinkBytes", counters.raw_payload_bytes as f64)?;
491 set_number(&object, "parseMs", parse_ms)?;
492 set_number(&object, "pipelineMs", pipeline_ms)?;
493 set_number(&object, "totalMs", total_ms)?;
494 Ok(object)
495}
496
497fn openipc_receiver_with_keypair_and_telemetry_channel_inner(
498 channel_id: u32,
499 telemetry_channel_id: u32,
500 keypair: WfbKeypair,
501 minimum_epoch: u64,
502) -> Result<OpenIpcReceiver, JsValue> {
503 let mut runtime = ReceiverRuntime::with_keyed_video_route(
504 FrameLayout::WithFcs,
505 VIDEO_ROUTE_ID,
506 ChannelId::new(channel_id),
507 DEFAULT_KEY_SLOT,
508 keypair,
509 minimum_epoch,
510 )
511 .map_err(|err| JsValue::from_str(&format!("invalid encrypted receiver config: {err}")))?;
512 runtime
513 .add_keyed_route(
514 TELEMETRY_ROUTE_ID,
515 ChannelId::new(telemetry_channel_id),
516 DEFAULT_KEY_SLOT,
517 keypair,
518 minimum_epoch,
519 )
520 .map_err(|err| JsValue::from_str(&format!("invalid MAVLink receiver config: {err}")))?;
521 Ok(OpenIpcReceiver::from_runtime(runtime))
522}
523
524pub(crate) fn parse_rx_descriptor_kind(kind: &str) -> Result<RxDescriptorKind, JsValue> {
525 match kind {
526 "jaguar1" | "rtl8812" | "rtl8821" | "rtl8814" => Ok(RxDescriptorKind::Jaguar1),
527 "jaguar3" | "rtl8812cu" | "rtl8812eu" | "rtl8822c" | "rtl8822cu" | "rtl8822e"
528 | "rtl8822eu" => Ok(RxDescriptorKind::Jaguar3),
529 _ => Err(JsValue::from_str(
530 "unsupported RX descriptor kind; expected jaguar1 or jaguar3",
531 )),
532 }
533}
534
535fn frame_bytes_array(frames: Vec<openipc_core::DepacketizedFrame>) -> Array {
536 let out = Array::new();
537 for frame in frames {
538 out.push(&Uint8Array::from(frame.data.as_slice()));
539 }
540 out
541}
542
543pub(crate) fn frame_objects_array(
544 frames: Vec<openipc_core::DepacketizedFrame>,
545) -> Result<Array, JsValue> {
546 let out = Array::new();
547 for frame in frames {
548 out.push(&video_frame_object(frame)?.into());
549 }
550 Ok(out)
551}
552
553pub(crate) fn raw_payload_array(
554 payloads: Vec<openipc_core::RoutePayload>,
555) -> Result<Array, JsValue> {
556 let out = Array::new();
557 for payload in payloads {
558 out.push(&raw_payload_object(payload)?.into());
559 }
560 Ok(out)
561}