1use js_sys::{Array, Uint8Array};
2use openipc_core::ieee80211::WifiFrame;
3use openipc_core::realtek::{parse_rx_aggregate, RxPacketType};
4use openipc_core::{
5 AdaptiveLinkSender, ChannelId, FecCounters, FrameLayout, RadioPort, WfbTxKeypair,
6};
7use wasm_bindgen::prelude::*;
8
9use crate::js::{counters_json, escape_json_str, ms_from_js};
10use crate::receiver::OpenIpcReceiver;
11#[cfg(target_arch = "wasm32")]
12use crate::webusb::WebUsbRealtekDevice;
13
14#[wasm_bindgen]
15pub struct OpenIpcAdaptiveLink {
20 sender: AdaptiveLinkSender,
21 last_counters: FecCounters,
22 rx_channel_id: ChannelId,
23}
24
25#[wasm_bindgen]
26impl OpenIpcAdaptiveLink {
27 #[wasm_bindgen(constructor)]
28 pub fn new(
30 link_id: u32,
31 keypair: &[u8],
32 epoch: u64,
33 fec_k: usize,
34 fec_n: usize,
35 ) -> Result<OpenIpcAdaptiveLink, JsValue> {
36 let keypair = WfbTxKeypair::from_bytes(keypair)
37 .map_err(|err| JsValue::from_str(&format!("invalid adaptive-link keypair: {err}")))?;
38 let sender = AdaptiveLinkSender::new(link_id, keypair, epoch, fec_k, fec_n)
39 .map_err(|err| JsValue::from_str(&format!("invalid adaptive-link config: {err}")))?;
40 Ok(Self {
41 sender,
42 last_counters: FecCounters::default(),
43 rx_channel_id: ChannelId::from_link_port(link_id, RadioPort::Video),
44 })
45 }
46
47 #[wasm_bindgen(js_name = recordRx)]
48 pub fn record_rx(&mut self, now_ms: f64, rssi0: u8, rssi1: u8, snr0: i8, snr1: i8) {
50 self.sender
51 .link_mut()
52 .record_rx(ms_from_js(now_ms), rssi0, rssi1, snr0, snr1);
53 }
54
55 #[wasm_bindgen(js_name = recordRxTransfer)]
56 pub fn record_rx_transfer(&mut self, transfer: &[u8], now_ms: f64) -> Result<(), JsValue> {
58 let packets = parse_rx_aggregate(transfer)
59 .map_err(|err| JsValue::from_str(&format!("Realtek RX aggregate rejected: {err}")))?;
60 let now_ms = ms_from_js(now_ms);
61 for packet in packets {
62 if packet.attrib.crc_err
63 || packet.attrib.icv_err
64 || packet.attrib.pkt_rpt_type != RxPacketType::NormalRx
65 {
66 continue;
67 }
68 if !WifiFrame::parse(packet.data, FrameLayout::WithFcs)
69 .map(|frame| frame.matches_channel_id(self.rx_channel_id))
70 .unwrap_or(false)
71 {
72 continue;
73 }
74 self.sender
75 .record_rx_paths(now_ms, packet.attrib.rssi, packet.attrib.snr);
76 }
77 Ok(())
78 }
79
80 #[wasm_bindgen(js_name = recordReceiverCounters)]
81 pub fn record_receiver_counters(&mut self, receiver: &OpenIpcReceiver, now_ms: f64) {
83 self.record_counter_delta(ms_from_js(now_ms), receiver.video_fec_counters());
84 }
85
86 #[wasm_bindgen(js_name = recordFec)]
87 pub fn record_fec(&mut self, now_ms: f64, total: u32, recovered: u32, lost: u32) {
89 self.sender
90 .record_fec(ms_from_js(now_ms), total, recovered, lost);
91 }
92
93 #[wasm_bindgen(js_name = requestKeyframe)]
94 pub fn request_keyframe(&mut self) {
96 self.sender.link_mut().request_keyframe();
97 }
98
99 #[wasm_bindgen(js_name = setKeyframeRequestMessages)]
100 pub fn set_keyframe_request_messages(&mut self, messages: u32) {
102 self.sender
103 .link_mut()
104 .set_keyframe_request_messages(messages);
105 }
106
107 #[wasm_bindgen(js_name = setVideoStartIdleMs)]
108 pub fn set_video_start_idle_ms(&mut self, idle_ms: u32) {
110 self.sender
111 .link_mut()
112 .set_video_start_idle_ms(idle_ms as u64);
113 }
114
115 #[wasm_bindgen(js_name = tick)]
116 pub fn tick(&mut self, now_ms: f64) -> Result<Array, JsValue> {
118 let frames = self
119 .sender
120 .tick(ms_from_js(now_ms))
121 .map_err(|err| JsValue::from_str(&format!("adaptive-link tick failed: {err}")))?;
122 let out = Array::new();
123 for frame in frames {
124 out.push(&Uint8Array::from(frame.as_slice()));
125 }
126 Ok(out)
127 }
128
129 #[wasm_bindgen(js_name = counters)]
130 pub fn counters(&self) -> String {
132 counters_json(self.last_counters)
133 }
134
135 #[wasm_bindgen(js_name = quality)]
136 pub fn quality(&mut self, now_ms: f64) -> String {
138 let quality = self.sender.link_mut().quality(ms_from_js(now_ms));
139 format!(
140 r#"{{"lostLastSecond":{},"recoveredLastSecond":{},"totalLastSecond":{},"rssi":[{},{}],"snr":[{},{}],"linkScore":[{},{}],"idrCode":"{}"}}"#,
141 quality.lost_last_second,
142 quality.recovered_last_second,
143 quality.total_last_second,
144 quality.rssi[0],
145 quality.rssi[1],
146 quality.snr[0],
147 quality.snr[1],
148 quality.link_score[0],
149 quality.link_score[1],
150 escape_json_str(&quality.idr_code),
151 )
152 }
153
154 fn record_counter_delta(&mut self, now_ms: u64, counters: FecCounters) {
155 let total = counters
156 .total_packets
157 .saturating_sub(self.last_counters.total_packets);
158 let recovered = counters
159 .recovered_packets
160 .saturating_sub(self.last_counters.recovered_packets);
161 let lost = counters
162 .lost_packets
163 .saturating_sub(self.last_counters.lost_packets);
164 self.last_counters = counters;
165 self.sender.record_fec(
166 now_ms,
167 total.min(u32::MAX as u64) as u32,
168 recovered.min(u32::MAX as u64) as u32,
169 lost.min(u32::MAX as u64) as u32,
170 );
171 }
172}
173
174#[cfg(target_arch = "wasm32")]
175#[wasm_bindgen]
176impl OpenIpcAdaptiveLink {
177 #[wasm_bindgen(js_name = tickAndSend)]
178 pub async fn tick_and_send(
180 &mut self,
181 device: &WebUsbRealtekDevice,
182 now_ms: f64,
183 current_channel: u8,
184 ) -> Result<usize, JsValue> {
185 let frames = self
186 .sender
187 .tick(ms_from_js(now_ms))
188 .map_err(|err| JsValue::from_str(&format!("adaptive-link tick failed: {err}")))?;
189 let count = frames.len();
190 for frame in frames {
191 device.send_packet(&frame, current_channel).await?;
192 }
193 Ok(count)
194 }
195}