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