Skip to main content

openipc_web/
adaptive.rs

1use js_sys::{Array, Uint8Array};
2use openipc_core::ieee80211::WifiFrame;
3use openipc_core::realtek::{parse_rx_aggregate_with_kind, RxDescriptorKind, 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::{parse_rx_descriptor_kind, OpenIpcReceiver};
11#[cfg(target_arch = "wasm32")]
12use crate::webusb::WebUsbRealtekDevice;
13
14#[wasm_bindgen]
15/// Browser/WASM adaptive-link feedback sender.
16///
17/// The app records RX quality and FEC counters, then calls `tick()` or
18/// `tickAndSend()` to produce/send encrypted WFB feedback packets.
19pub struct OpenIpcAdaptiveLink {
20    sender: AdaptiveLinkSender,
21    last_counters: FecCounters,
22    rx_channel_id: ChannelId,
23    rx_descriptor_kind: RxDescriptorKind,
24}
25
26impl OpenIpcAdaptiveLink {
27    #[cfg(target_arch = "wasm32")]
28    pub(crate) fn record_rx_paths(&mut self, now_ms: u64, rssi: [u8; 4], snr: [i8; 4]) {
29        self.sender.record_rx_paths(now_ms, rssi, snr);
30    }
31
32    #[cfg(target_arch = "wasm32")]
33    pub(crate) fn record_counters(&mut self, now_ms: u64, counters: FecCounters) {
34        self.record_counter_delta(now_ms, counters);
35    }
36}
37
38#[wasm_bindgen]
39impl OpenIpcAdaptiveLink {
40    #[wasm_bindgen(constructor)]
41    /// Create a new adaptive-link sender for a link id and WFB TX keypair.
42    pub fn new(
43        link_id: u32,
44        keypair: &[u8],
45        epoch: u64,
46        fec_k: usize,
47        fec_n: usize,
48    ) -> Result<OpenIpcAdaptiveLink, JsValue> {
49        let keypair = WfbTxKeypair::from_bytes(keypair)
50            .map_err(|err| JsValue::from_str(&format!("invalid adaptive-link keypair: {err}")))?;
51        let sender = AdaptiveLinkSender::new(link_id, keypair, epoch, fec_k, fec_n)
52            .map_err(|err| JsValue::from_str(&format!("invalid adaptive-link config: {err}")))?;
53        Ok(Self {
54            sender,
55            last_counters: FecCounters::default(),
56            rx_channel_id: ChannelId::from_link_port(link_id, RadioPort::Video),
57            rx_descriptor_kind: RxDescriptorKind::Jaguar1,
58        })
59    }
60
61    #[wasm_bindgen(js_name = setRxDescriptorKind)]
62    /// Select the Realtek USB RX descriptor layout for future RSSI/SNR sampling.
63    pub fn set_rx_descriptor_kind(&mut self, kind: &str) -> Result<(), JsValue> {
64        self.rx_descriptor_kind = parse_rx_descriptor_kind(kind)?;
65        Ok(())
66    }
67
68    #[wasm_bindgen(js_name = recordRx)]
69    /// Record one pair of RSSI/SNR samples for link-quality estimation.
70    pub fn record_rx(&mut self, now_ms: f64, rssi0: u8, rssi1: u8, snr0: i8, snr1: i8) {
71        self.sender
72            .link_mut()
73            .record_rx(ms_from_js(now_ms), rssi0, rssi1, snr0, snr1);
74    }
75
76    #[wasm_bindgen(js_name = recordRxTransfer)]
77    /// Parse one RX transfer and record RSSI/SNR for matching video frames.
78    pub fn record_rx_transfer(&mut self, transfer: &[u8], now_ms: f64) -> Result<(), JsValue> {
79        let packets = parse_rx_aggregate_with_kind(transfer, self.rx_descriptor_kind)
80            .map_err(|err| JsValue::from_str(&format!("Realtek RX aggregate rejected: {err}")))?;
81        let now_ms = ms_from_js(now_ms);
82        for packet in packets {
83            if packet.attrib.crc_err
84                || packet.attrib.icv_err
85                || packet.attrib.pkt_rpt_type != RxPacketType::NormalRx
86            {
87                continue;
88            }
89            if !WifiFrame::parse(packet.data, FrameLayout::WithFcs)
90                .map(|frame| frame.matches_channel_id(self.rx_channel_id))
91                .unwrap_or(false)
92            {
93                continue;
94            }
95            self.sender
96                .record_rx_paths(now_ms, packet.attrib.rssi, packet.attrib.snr);
97        }
98        Ok(())
99    }
100
101    #[wasm_bindgen(js_name = recordReceiverCounters)]
102    /// Record FEC counter deltas from an [`OpenIpcReceiver`].
103    pub fn record_receiver_counters(&mut self, receiver: &OpenIpcReceiver, now_ms: f64) {
104        self.record_counter_delta(ms_from_js(now_ms), receiver.video_fec_counters());
105    }
106
107    #[wasm_bindgen(js_name = recordFec)]
108    /// Record explicit FEC totals for the current time window.
109    pub fn record_fec(&mut self, now_ms: f64, total: u32, recovered: u32, lost: u32) {
110        self.sender
111            .record_fec(ms_from_js(now_ms), total, recovered, lost);
112    }
113
114    #[wasm_bindgen(js_name = requestKeyframe)]
115    /// Force keyframe-request messages in upcoming feedback packets.
116    pub fn request_keyframe(&mut self) {
117        self.sender.link_mut().request_keyframe();
118    }
119
120    #[wasm_bindgen(js_name = setKeyframeRequestMessages)]
121    /// Configure how many feedback packets carry a keyframe request.
122    pub fn set_keyframe_request_messages(&mut self, messages: u32) {
123        self.sender
124            .link_mut()
125            .set_keyframe_request_messages(messages);
126    }
127
128    #[wasm_bindgen(js_name = setVideoStartIdleMs)]
129    /// Configure how long a quiet video stream is considered idle.
130    pub fn set_video_start_idle_ms(&mut self, idle_ms: u32) {
131        self.sender
132            .link_mut()
133            .set_video_start_idle_ms(idle_ms as u64);
134    }
135
136    #[wasm_bindgen(js_name = tick)]
137    /// Return feedback frames that should be sent at `now_ms`.
138    pub fn tick(&mut self, now_ms: f64) -> Result<Array, JsValue> {
139        let frames = self
140            .sender
141            .tick(ms_from_js(now_ms))
142            .map_err(|err| JsValue::from_str(&format!("adaptive-link tick failed: {err}")))?;
143        let out = Array::new();
144        for frame in frames {
145            out.push(&Uint8Array::from(frame.as_slice()));
146        }
147        Ok(out)
148    }
149
150    #[wasm_bindgen(js_name = counters)]
151    /// Return the last recorded FEC counters as JSON.
152    pub fn counters(&self) -> String {
153        counters_json(self.last_counters)
154    }
155
156    #[wasm_bindgen(js_name = quality)]
157    /// Return the current link-quality report as JSON.
158    pub fn quality(&mut self, now_ms: f64) -> String {
159        let quality = self.sender.link_mut().quality(ms_from_js(now_ms));
160        format!(
161            r#"{{"lostLastSecond":{},"recoveredLastSecond":{},"totalLastSecond":{},"rssi":[{},{}],"snr":[{},{}],"linkScore":[{},{}],"idrCode":"{}"}}"#,
162            quality.lost_last_second,
163            quality.recovered_last_second,
164            quality.total_last_second,
165            quality.rssi[0],
166            quality.rssi[1],
167            quality.snr[0],
168            quality.snr[1],
169            quality.link_score[0],
170            quality.link_score[1],
171            escape_json_str(&quality.idr_code),
172        )
173    }
174
175    fn record_counter_delta(&mut self, now_ms: u64, counters: FecCounters) {
176        let total = counters
177            .total_packets
178            .saturating_sub(self.last_counters.total_packets);
179        let recovered = counters
180            .recovered_packets
181            .saturating_sub(self.last_counters.recovered_packets);
182        let lost = counters
183            .lost_packets
184            .saturating_sub(self.last_counters.lost_packets);
185        self.last_counters = counters;
186        self.sender.record_fec(
187            now_ms,
188            total.min(u32::MAX as u64) as u32,
189            recovered.min(u32::MAX as u64) as u32,
190            lost.min(u32::MAX as u64) as u32,
191        );
192    }
193}
194
195#[cfg(target_arch = "wasm32")]
196#[wasm_bindgen]
197impl OpenIpcAdaptiveLink {
198    #[wasm_bindgen(js_name = tickAndSend)]
199    /// Produce due feedback frames and send them through a WebUSB Realtek device.
200    pub async fn tick_and_send(
201        &mut self,
202        device: &WebUsbRealtekDevice,
203        now_ms: f64,
204        current_channel: u8,
205    ) -> Result<usize, JsValue> {
206        self.tick_and_send_for_radio(device, now_ms, current_channel, 20)
207            .await
208    }
209
210    #[wasm_bindgen(js_name = tickAndSendForRadio)]
211    /// Produce due feedback frames with the adapter's configured RF width.
212    pub async fn tick_and_send_for_radio(
213        &mut self,
214        device: &WebUsbRealtekDevice,
215        now_ms: f64,
216        current_channel: u8,
217        channel_width_mhz: u16,
218    ) -> Result<usize, JsValue> {
219        let frames = self
220            .sender
221            .tick(ms_from_js(now_ms))
222            .map_err(|err| JsValue::from_str(&format!("adaptive-link tick failed: {err}")))?;
223        let count = frames.len();
224        for frame in frames {
225            device
226                .send_packet_for_radio(&frame, current_channel, channel_width_mhz, false)
227                .await?;
228        }
229        Ok(count)
230    }
231}