Skip to main content

openipc_web/
lib.rs

1use js_sys::{Array, Object, Reflect, Uint8Array};
2use openipc_core::ieee80211::WifiFrame;
3use openipc_core::realtek::{parse_rx_aggregate, RxPacketType};
4#[cfg(target_arch = "wasm32")]
5use openipc_core::realtek_tx::RealtekTxOptions;
6use openipc_core::{
7    AdaptiveLinkSender, ChannelId, Codec, DepacketizedFrame, FecCounters, FrameLayout,
8    PipelineEvent, RadioPort, ReceiverPipeline, WfbKeypair, WfbTxKeypair,
9};
10#[cfg(target_arch = "wasm32")]
11use openipc_rtl88xx::{ChannelWidth, InitReport, InitStatus, RadioConfig, RealtekDevice};
12use wasm_bindgen::prelude::*;
13
14#[wasm_bindgen(typescript_custom_section)]
15const OPENIPC_VIDEO_FRAME_TYPES: &'static str = r#"
16export type OpenIpcVideoFrame = {
17    data: Uint8Array;
18    codec: "h264" | "h265";
19    codecString: string;
20    isKeyFrame: boolean;
21    timestamp: number;
22};
23
24export type OpenIpcRxTransferProfile = {
25    frames: OpenIpcVideoFrame[];
26    transferBytes: number;
27    packets: number;
28    acceptedPackets: number;
29    droppedPackets: number;
30    crcDropped: number;
31    icvDropped: number;
32    reportDropped: number;
33    ignoredFrames: number;
34    sessions: number;
35    wfbPayloads: number;
36    rtpPackets: number;
37    videoFrames: number;
38    parseMs: number;
39    pipelineMs: number;
40    totalMs: number;
41};
42"#;
43
44#[wasm_bindgen]
45pub struct OpenIpcReceiver {
46    pipeline: ReceiverPipeline,
47}
48
49#[wasm_bindgen]
50impl OpenIpcReceiver {
51    #[wasm_bindgen(constructor)]
52    pub fn new() -> Result<OpenIpcReceiver, JsValue> {
53        Self::with_channel_id(openipc_core::channel::DEFAULT_LINK_ID << 8, 1, 5)
54    }
55
56    #[wasm_bindgen(js_name = withChannelId)]
57    pub fn with_channel_id(
58        channel_id: u32,
59        fec_k: usize,
60        fec_n: usize,
61    ) -> Result<OpenIpcReceiver, JsValue> {
62        let pipeline = ReceiverPipeline::new(
63            ChannelId::new(channel_id),
64            FrameLayout::WithFcs,
65            fec_k,
66            fec_n,
67        )
68        .map_err(|err| JsValue::from_str(&format!("invalid receiver config: {err:?}")))?;
69        Ok(Self { pipeline })
70    }
71
72    #[wasm_bindgen(js_name = withKeypair)]
73    pub fn with_keypair(
74        channel_id: u32,
75        keypair: &[u8],
76        minimum_epoch: u64,
77    ) -> Result<OpenIpcReceiver, JsValue> {
78        let keypair = WfbKeypair::from_bytes(keypair)
79            .map_err(|err| JsValue::from_str(&format!("invalid WFB keypair: {err}")))?;
80        let pipeline = ReceiverPipeline::with_keypair(
81            ChannelId::new(channel_id),
82            FrameLayout::WithFcs,
83            keypair,
84            minimum_epoch,
85        )
86        .map_err(|err| JsValue::from_str(&format!("invalid encrypted receiver config: {err}")))?;
87        Ok(Self { pipeline })
88    }
89
90    #[wasm_bindgen(js_name = pushRtpPacket)]
91    pub fn push_rtp_packet(&mut self, data: &[u8]) -> Option<Uint8Array> {
92        self.pipeline
93            .push_rtp(data)
94            .map(|frame| Uint8Array::from(frame.data.as_slice()))
95    }
96
97    #[wasm_bindgen(
98        js_name = pushRtpPacketDetailed,
99        unchecked_return_type = "OpenIpcVideoFrame | null"
100    )]
101    pub fn push_rtp_packet_detailed(&mut self, data: &[u8]) -> Result<JsValue, JsValue> {
102        match self.pipeline.push_rtp(data) {
103            Some(frame) => Ok(video_frame_object(frame)?.into()),
104            None => Ok(JsValue::NULL),
105        }
106    }
107
108    #[wasm_bindgen(js_name = pushDecryptedFragment)]
109    pub fn push_decrypted_fragment(
110        &mut self,
111        data_nonce_hex: &str,
112        fragment: &[u8],
113    ) -> Result<Array, JsValue> {
114        let data_nonce = parse_hex_u64(data_nonce_hex)?;
115        let events = self
116            .pipeline
117            .push_decrypted_fragment(data_nonce, fragment)
118            .map_err(|err| JsValue::from_str(&format!("WFB fragment rejected: {err:?}")))?;
119
120        let frames = Array::new();
121        for event in events {
122            if let PipelineEvent::VideoFrame(frame) = event {
123                frames.push(&Uint8Array::from(frame.data.as_slice()));
124            }
125        }
126        Ok(frames)
127    }
128
129    #[wasm_bindgen(js_name = pushDecrypted80211Frame)]
130    pub fn push_decrypted_80211_frame(
131        &mut self,
132        frame: &[u8],
133        fragment: &[u8],
134    ) -> Result<Array, JsValue> {
135        let events = self
136            .pipeline
137            .push_decrypted_80211_frame(frame, fragment)
138            .map_err(|err| JsValue::from_str(&format!("802.11 frame rejected: {err:?}")))?;
139        let frames = Array::new();
140        for event in events {
141            if let PipelineEvent::VideoFrame(frame) = event {
142                frames.push(&Uint8Array::from(frame.data.as_slice()));
143            }
144        }
145        Ok(frames)
146    }
147
148    #[wasm_bindgen(js_name = pushEncrypted80211Frame)]
149    pub fn push_encrypted_80211_frame(&mut self, frame: &[u8]) -> Result<Array, JsValue> {
150        let events = self
151            .pipeline
152            .push_80211_frame(frame)
153            .map_err(|err| JsValue::from_str(&format!("802.11 frame rejected: {err}")))?;
154        Ok(video_frames_from_events(events))
155    }
156
157    #[wasm_bindgen(js_name = pushRxTransfer)]
158    pub fn push_rx_transfer(&mut self, transfer: &[u8]) -> Result<Array, JsValue> {
159        let packets = parse_rx_aggregate(transfer)
160            .map_err(|err| JsValue::from_str(&format!("Realtek RX aggregate rejected: {err}")))?;
161        let frames = Array::new();
162        for packet in packets {
163            if packet.attrib.crc_err
164                || packet.attrib.icv_err
165                || packet.attrib.pkt_rpt_type != RxPacketType::NormalRx
166            {
167                continue;
168            }
169            let events = self
170                .pipeline
171                .push_80211_frame(packet.data)
172                .map_err(|err| JsValue::from_str(&format!("OpenIPC frame rejected: {err}")))?;
173            append_video_frames(&frames, events);
174        }
175        Ok(frames)
176    }
177
178    #[wasm_bindgen(
179        js_name = pushRxTransferDetailed,
180        unchecked_return_type = "OpenIpcVideoFrame[]"
181    )]
182    pub fn push_rx_transfer_detailed(&mut self, transfer: &[u8]) -> Result<Array, JsValue> {
183        let packets = parse_rx_aggregate(transfer)
184            .map_err(|err| JsValue::from_str(&format!("Realtek RX aggregate rejected: {err}")))?;
185        let frames = Array::new();
186        for packet in packets {
187            if packet.attrib.crc_err
188                || packet.attrib.icv_err
189                || packet.attrib.pkt_rpt_type != RxPacketType::NormalRx
190            {
191                continue;
192            }
193            let events = self
194                .pipeline
195                .push_80211_frame(packet.data)
196                .map_err(|err| JsValue::from_str(&format!("OpenIPC frame rejected: {err}")))?;
197            append_video_frame_objects(&frames, events)?;
198        }
199        Ok(frames)
200    }
201
202    #[wasm_bindgen(
203        js_name = pushRxTransferProfiled,
204        unchecked_return_type = "OpenIpcRxTransferProfile"
205    )]
206    pub fn push_rx_transfer_profiled(&mut self, transfer: &[u8]) -> Result<Object, JsValue> {
207        let total_start = now_ms();
208        let parse_start = now_ms();
209        let packets = parse_rx_aggregate(transfer)
210            .map_err(|err| JsValue::from_str(&format!("Realtek RX aggregate rejected: {err}")))?;
211        let parse_ms = elapsed_ms(parse_start);
212
213        let frames = Array::new();
214        let mut accepted_packets = 0usize;
215        let mut crc_dropped = 0usize;
216        let mut icv_dropped = 0usize;
217        let mut report_dropped = 0usize;
218        let mut ignored_frames = 0usize;
219        let mut sessions = 0usize;
220        let mut wfb_payloads = 0usize;
221        let mut rtp_packets = 0usize;
222        let mut video_frames = 0usize;
223
224        let pipeline_start = now_ms();
225        let packet_count = packets.len();
226        for packet in packets {
227            if packet.attrib.crc_err {
228                crc_dropped += 1;
229                continue;
230            }
231            if packet.attrib.icv_err {
232                icv_dropped += 1;
233                continue;
234            }
235            if packet.attrib.pkt_rpt_type != RxPacketType::NormalRx {
236                report_dropped += 1;
237                continue;
238            }
239            accepted_packets += 1;
240            let events = self
241                .pipeline
242                .push_80211_frame(packet.data)
243                .map_err(|err| JsValue::from_str(&format!("OpenIPC frame rejected: {err}")))?;
244            for event in events {
245                match event {
246                    PipelineEvent::IgnoredFrame => ignored_frames += 1,
247                    PipelineEvent::SessionEstablished { .. } => sessions += 1,
248                    PipelineEvent::WfbPayload { .. } => wfb_payloads += 1,
249                    PipelineEvent::RtpPacket { .. } => rtp_packets += 1,
250                    PipelineEvent::VideoFrame(frame) => {
251                        video_frames += 1;
252                        frames.push(&video_frame_object(frame)?.into());
253                    }
254                }
255            }
256        }
257        let pipeline_ms = elapsed_ms(pipeline_start);
258
259        let object = Object::new();
260        Reflect::set(&object, &JsValue::from_str("frames"), &frames)?;
261        set_number(&object, "transferBytes", transfer.len() as f64)?;
262        set_number(&object, "packets", packet_count as f64)?;
263        set_number(&object, "acceptedPackets", accepted_packets as f64)?;
264        set_number(
265            &object,
266            "droppedPackets",
267            (crc_dropped + icv_dropped + report_dropped) as f64,
268        )?;
269        set_number(&object, "crcDropped", crc_dropped as f64)?;
270        set_number(&object, "icvDropped", icv_dropped as f64)?;
271        set_number(&object, "reportDropped", report_dropped as f64)?;
272        set_number(&object, "ignoredFrames", ignored_frames as f64)?;
273        set_number(&object, "sessions", sessions as f64)?;
274        set_number(&object, "wfbPayloads", wfb_payloads as f64)?;
275        set_number(&object, "rtpPackets", rtp_packets as f64)?;
276        set_number(&object, "videoFrames", video_frames as f64)?;
277        set_number(&object, "parseMs", parse_ms)?;
278        set_number(&object, "pipelineMs", pipeline_ms)?;
279        set_number(&object, "totalMs", elapsed_ms(total_start))?;
280        Ok(object)
281    }
282
283    #[wasm_bindgen(js_name = fecCounters)]
284    pub fn fec_counters(&self) -> String {
285        counters_json(self.pipeline.fec_counters())
286    }
287}
288
289#[wasm_bindgen]
290pub struct OpenIpcAdaptiveLink {
291    sender: AdaptiveLinkSender,
292    last_counters: FecCounters,
293    rx_channel_id: ChannelId,
294}
295
296#[wasm_bindgen]
297impl OpenIpcAdaptiveLink {
298    #[wasm_bindgen(constructor)]
299    pub fn new(
300        link_id: u32,
301        keypair: &[u8],
302        epoch: u64,
303        fec_k: usize,
304        fec_n: usize,
305    ) -> Result<OpenIpcAdaptiveLink, JsValue> {
306        let keypair = WfbTxKeypair::from_bytes(keypair)
307            .map_err(|err| JsValue::from_str(&format!("invalid adaptive-link keypair: {err}")))?;
308        let sender = AdaptiveLinkSender::new(link_id, keypair, epoch, fec_k, fec_n)
309            .map_err(|err| JsValue::from_str(&format!("invalid adaptive-link config: {err}")))?;
310        Ok(Self {
311            sender,
312            last_counters: FecCounters::default(),
313            rx_channel_id: ChannelId::from_link_port(link_id, RadioPort::Video),
314        })
315    }
316
317    #[wasm_bindgen(js_name = recordRx)]
318    pub fn record_rx(&mut self, now_ms: f64, rssi0: u8, rssi1: u8, snr0: i8, snr1: i8) {
319        self.sender
320            .link_mut()
321            .record_rx(ms_from_js(now_ms), rssi0, rssi1, snr0, snr1);
322    }
323
324    #[wasm_bindgen(js_name = recordRxTransfer)]
325    pub fn record_rx_transfer(&mut self, transfer: &[u8], now_ms: f64) -> Result<(), JsValue> {
326        let packets = parse_rx_aggregate(transfer)
327            .map_err(|err| JsValue::from_str(&format!("Realtek RX aggregate rejected: {err}")))?;
328        let now_ms = ms_from_js(now_ms);
329        for packet in packets {
330            if packet.attrib.crc_err
331                || packet.attrib.icv_err
332                || packet.attrib.pkt_rpt_type != RxPacketType::NormalRx
333            {
334                continue;
335            }
336            if !WifiFrame::parse(packet.data, FrameLayout::WithFcs)
337                .map(|frame| frame.matches_channel_id(self.rx_channel_id))
338                .unwrap_or(false)
339            {
340                continue;
341            }
342            self.sender
343                .record_rx_paths(now_ms, packet.attrib.rssi, packet.attrib.snr);
344        }
345        Ok(())
346    }
347
348    #[wasm_bindgen(js_name = recordReceiverCounters)]
349    pub fn record_receiver_counters(&mut self, receiver: &OpenIpcReceiver, now_ms: f64) {
350        self.record_counter_delta(ms_from_js(now_ms), receiver.pipeline.fec_counters());
351    }
352
353    #[wasm_bindgen(js_name = recordFec)]
354    pub fn record_fec(&mut self, now_ms: f64, total: u32, recovered: u32, lost: u32) {
355        self.sender
356            .record_fec(ms_from_js(now_ms), total, recovered, lost);
357    }
358
359    #[wasm_bindgen(js_name = requestKeyframe)]
360    pub fn request_keyframe(&mut self) {
361        self.sender.link_mut().request_keyframe();
362    }
363
364    #[wasm_bindgen(js_name = setKeyframeRequestMessages)]
365    pub fn set_keyframe_request_messages(&mut self, messages: u32) {
366        self.sender
367            .link_mut()
368            .set_keyframe_request_messages(messages);
369    }
370
371    #[wasm_bindgen(js_name = setVideoStartIdleMs)]
372    pub fn set_video_start_idle_ms(&mut self, idle_ms: u32) {
373        self.sender
374            .link_mut()
375            .set_video_start_idle_ms(idle_ms as u64);
376    }
377
378    #[wasm_bindgen(js_name = tick)]
379    pub fn tick(&mut self, now_ms: f64) -> Result<Array, JsValue> {
380        let frames = self
381            .sender
382            .tick(ms_from_js(now_ms))
383            .map_err(|err| JsValue::from_str(&format!("adaptive-link tick failed: {err}")))?;
384        let out = Array::new();
385        for frame in frames {
386            out.push(&Uint8Array::from(frame.as_slice()));
387        }
388        Ok(out)
389    }
390
391    #[wasm_bindgen(js_name = counters)]
392    pub fn counters(&self) -> String {
393        counters_json(self.last_counters)
394    }
395
396    #[wasm_bindgen(js_name = quality)]
397    pub fn quality(&mut self, now_ms: f64) -> String {
398        let quality = self.sender.link_mut().quality(ms_from_js(now_ms));
399        format!(
400            r#"{{"lostLastSecond":{},"recoveredLastSecond":{},"totalLastSecond":{},"rssi":[{},{}],"snr":[{},{}],"linkScore":[{},{}],"idrCode":"{}"}}"#,
401            quality.lost_last_second,
402            quality.recovered_last_second,
403            quality.total_last_second,
404            quality.rssi[0],
405            quality.rssi[1],
406            quality.snr[0],
407            quality.snr[1],
408            quality.link_score[0],
409            quality.link_score[1],
410            escape_json_str(&quality.idr_code),
411        )
412    }
413
414    fn record_counter_delta(&mut self, now_ms: u64, counters: FecCounters) {
415        let total = counters
416            .total_packets
417            .saturating_sub(self.last_counters.total_packets);
418        let recovered = counters
419            .recovered_packets
420            .saturating_sub(self.last_counters.recovered_packets);
421        let lost = counters
422            .lost_packets
423            .saturating_sub(self.last_counters.lost_packets);
424        self.last_counters = counters;
425        self.sender.record_fec(
426            now_ms,
427            total.min(u32::MAX as u64) as u32,
428            recovered.min(u32::MAX as u64) as u32,
429            lost.min(u32::MAX as u64) as u32,
430        );
431    }
432}
433
434#[cfg(target_arch = "wasm32")]
435#[wasm_bindgen]
436impl OpenIpcAdaptiveLink {
437    #[wasm_bindgen(js_name = tickAndSend)]
438    pub async fn tick_and_send(
439        &mut self,
440        device: &WebUsbRealtekDevice,
441        now_ms: f64,
442        current_channel: u8,
443    ) -> Result<usize, JsValue> {
444        let frames = self
445            .sender
446            .tick(ms_from_js(now_ms))
447            .map_err(|err| JsValue::from_str(&format!("adaptive-link tick failed: {err}")))?;
448        let count = frames.len();
449        for frame in frames {
450            device.send_packet(&frame, current_channel).await?;
451        }
452        Ok(count)
453    }
454}
455
456#[cfg(target_arch = "wasm32")]
457#[wasm_bindgen]
458pub struct WebUsbRealtekDevice {
459    driver: RealtekDevice,
460}
461
462#[cfg(target_arch = "wasm32")]
463#[wasm_bindgen]
464impl WebUsbRealtekDevice {
465    #[wasm_bindgen(js_name = fromWebUsbDevice)]
466    pub async fn from_web_usb_device(
467        device: web_sys::UsbDevice,
468    ) -> Result<WebUsbRealtekDevice, JsValue> {
469        let driver = RealtekDevice::from_web_usb_device(device)
470            .await
471            .map_err(driver_error)?;
472        Ok(Self { driver })
473    }
474
475    #[wasm_bindgen(js_name = bulkInEndpoint)]
476    pub fn bulk_in_endpoint(&self) -> u8 {
477        self.driver.bulk_in_ep
478    }
479
480    #[wasm_bindgen(js_name = bulkOutEndpoint)]
481    pub fn bulk_out_endpoint(&self) -> u8 {
482        self.driver.bulk_out_ep
483    }
484
485    #[wasm_bindgen(js_name = initializeMonitor)]
486    pub async fn initialize_monitor(
487        &self,
488        channel: u8,
489        channel_width_mhz: u16,
490        channel_offset: u8,
491    ) -> Result<String, JsValue> {
492        let radio = RadioConfig {
493            channel,
494            channel_offset,
495            channel_width: parse_channel_width(channel_width_mhz)?,
496        };
497        let report = self
498            .driver
499            .initialize_monitor_async(radio, false)
500            .await
501            .map_err(driver_error)?;
502        Ok(init_report_json(&report))
503    }
504
505    #[wasm_bindgen(js_name = readRxTransfer)]
506    pub async fn read_rx_transfer(&self, length: usize) -> Result<Uint8Array, JsValue> {
507        let bytes = self
508            .driver
509            .read_rx_transfer_async(length)
510            .await
511            .map_err(driver_error)?;
512        Ok(Uint8Array::from(bytes.as_slice()))
513    }
514
515    #[wasm_bindgen(js_name = writeTxTransfer)]
516    pub async fn write_tx_transfer(&self, transfer: &[u8]) -> Result<usize, JsValue> {
517        self.driver
518            .write_tx_transfer_async(transfer)
519            .await
520            .map_err(driver_error)
521    }
522
523    #[wasm_bindgen(js_name = sendPacket)]
524    pub async fn send_packet(
525        &self,
526        radiotap_packet: &[u8],
527        current_channel: u8,
528    ) -> Result<usize, JsValue> {
529        let chip = self.driver.probe_chip_async().await.map_err(driver_error)?;
530        self.driver
531            .send_packet_async(
532                radiotap_packet,
533                RealtekTxOptions {
534                    current_channel,
535                    is_8814a: chip.family == openipc_rtl88xx::ChipFamily::Rtl8814,
536                },
537            )
538            .await
539            .map_err(driver_error)
540    }
541
542    #[wasm_bindgen(js_name = setTxPowerOverride)]
543    pub async fn set_tx_power_override(
544        &self,
545        current_channel: u8,
546        power: u8,
547    ) -> Result<(), JsValue> {
548        self.driver
549            .set_tx_power_override_async(current_channel, power)
550            .await
551            .map_err(driver_error)
552    }
553
554    #[wasm_bindgen(js_name = readRegisterU8)]
555    pub async fn read_register_u8(&self, register: u16) -> Result<u8, JsValue> {
556        self.driver
557            .read_u8_async(register)
558            .await
559            .map_err(driver_error)
560    }
561
562    #[wasm_bindgen(js_name = readRegisterU32)]
563    pub async fn read_register_u32(&self, register: u16) -> Result<u32, JsValue> {
564        self.driver
565            .read_u32_async(register)
566            .await
567            .map_err(driver_error)
568    }
569}
570
571#[cfg(target_arch = "wasm32")]
572fn parse_channel_width(width_mhz: u16) -> Result<ChannelWidth, JsValue> {
573    match width_mhz {
574        20 => Ok(ChannelWidth::Mhz20),
575        40 => Ok(ChannelWidth::Mhz40),
576        80 => Ok(ChannelWidth::Mhz80),
577        _ => Err(JsValue::from_str(
578            "unsupported channel width; expected 20, 40, or 80 MHz",
579        )),
580    }
581}
582
583#[cfg(target_arch = "wasm32")]
584fn init_report_json(report: &InitReport) -> String {
585    let status = match report.status {
586        InitStatus::AlreadyRunning => "already_running",
587        InitStatus::Initialized => "initialized",
588    };
589    format!(
590        r#"{{"chip":"{}","rfPaths":{},"cutVersion":{},"status":"{}","firmwareDownloaded":{}}}"#,
591        report.chip.family.name(),
592        report.chip.total_rf_paths(),
593        report.chip.cut_version,
594        status,
595        report.firmware_downloaded
596    )
597}
598
599#[cfg(target_arch = "wasm32")]
600fn driver_error(err: impl std::fmt::Display) -> JsValue {
601    JsValue::from_str(&err.to_string())
602}
603
604fn video_frames_from_events(events: Vec<PipelineEvent>) -> Array {
605    let frames = Array::new();
606    append_video_frames(&frames, events);
607    frames
608}
609
610fn append_video_frames(frames: &Array, events: Vec<PipelineEvent>) {
611    for event in events {
612        if let PipelineEvent::VideoFrame(frame) = event {
613            frames.push(&Uint8Array::from(frame.data.as_slice()));
614        }
615    }
616}
617
618fn append_video_frame_objects(frames: &Array, events: Vec<PipelineEvent>) -> Result<(), JsValue> {
619    for event in events {
620        if let PipelineEvent::VideoFrame(frame) = event {
621            frames.push(&video_frame_object(frame)?.into());
622        }
623    }
624    Ok(())
625}
626
627fn video_frame_object(frame: DepacketizedFrame) -> Result<Object, JsValue> {
628    let object = Object::new();
629    let codec_string = codec_string(&frame);
630    Reflect::set(
631        &object,
632        &JsValue::from_str("data"),
633        &Uint8Array::from(frame.data.as_slice()),
634    )?;
635    Reflect::set(
636        &object,
637        &JsValue::from_str("codec"),
638        &JsValue::from_str(codec_name(frame.codec)),
639    )?;
640    Reflect::set(
641        &object,
642        &JsValue::from_str("codecString"),
643        &JsValue::from_str(&codec_string),
644    )?;
645    Reflect::set(
646        &object,
647        &JsValue::from_str("isKeyFrame"),
648        &JsValue::from_bool(frame.is_keyframe),
649    )?;
650    Reflect::set(
651        &object,
652        &JsValue::from_str("timestamp"),
653        &JsValue::from_f64(f64::from(frame.timestamp)),
654    )?;
655    Ok(object)
656}
657
658fn codec_name(codec: Codec) -> &'static str {
659    match codec {
660        Codec::H264 => "h264",
661        Codec::H265 => "h265",
662    }
663}
664
665fn codec_string(frame: &DepacketizedFrame) -> String {
666    match frame.codec {
667        Codec::H264 => h264_codec_string(&frame.data).unwrap_or_else(|| "avc1.42E01E".to_owned()),
668        Codec::H265 => "hev1.1.6.L93.B0".to_owned(),
669    }
670}
671
672fn h264_codec_string(frame: &[u8]) -> Option<String> {
673    for unit in annex_b_units(frame) {
674        let nalu = &frame[unit.start..unit.end];
675        if nalu.len() >= 4 && nalu[0] & 0x1f == 7 {
676            return Some(format!(
677                "avc1.{}{}{}",
678                hex_byte(nalu[1]),
679                hex_byte(nalu[2]),
680                hex_byte(nalu[3])
681            ));
682        }
683    }
684    None
685}
686
687#[derive(Debug, Clone, Copy)]
688struct AnnexBUnit {
689    start: usize,
690    end: usize,
691}
692
693fn annex_b_units(frame: &[u8]) -> Vec<AnnexBUnit> {
694    let mut starts = Vec::new();
695    let mut index = 0;
696    while index + 3 < frame.len() {
697        let len = start_code_len(frame, index);
698        if len > 0 {
699            starts.push(index);
700            index += len;
701        } else {
702            index += 1;
703        }
704    }
705    if starts.is_empty() && !frame.is_empty() {
706        return vec![AnnexBUnit {
707            start: 0,
708            end: frame.len(),
709        }];
710    }
711    starts
712        .iter()
713        .enumerate()
714        .map(|(index, start)| AnnexBUnit {
715            start: start + start_code_len(frame, *start),
716            end: starts.get(index + 1).copied().unwrap_or(frame.len()),
717        })
718        .collect()
719}
720
721fn start_code_len(frame: &[u8], offset: usize) -> usize {
722    if frame.get(offset) != Some(&0) || frame.get(offset + 1) != Some(&0) {
723        return 0;
724    }
725    if frame.get(offset + 2) == Some(&1) {
726        return 3;
727    }
728    if frame.get(offset + 2) == Some(&0) && frame.get(offset + 3) == Some(&1) {
729        return 4;
730    }
731    0
732}
733
734fn hex_byte(value: u8) -> String {
735    format!("{value:02X}")
736}
737
738fn set_number(object: &Object, key: &str, value: f64) -> Result<(), JsValue> {
739    Reflect::set(object, &JsValue::from_str(key), &JsValue::from_f64(value))?;
740    Ok(())
741}
742
743fn now_ms() -> f64 {
744    #[cfg(target_arch = "wasm32")]
745    {
746        web_sys::window()
747            .and_then(|window| window.performance())
748            .map(|performance| performance.now())
749            .unwrap_or_else(js_sys::Date::now)
750    }
751    #[cfg(not(target_arch = "wasm32"))]
752    {
753        0.0
754    }
755}
756
757fn elapsed_ms(start_ms: f64) -> f64 {
758    let elapsed = now_ms() - start_ms;
759    if elapsed.is_finite() && elapsed >= 0.0 {
760        elapsed
761    } else {
762        0.0
763    }
764}
765
766fn counters_json(counters: FecCounters) -> String {
767    format!(
768        r#"{{"totalPackets":{},"recoveredPackets":{},"lostPackets":{},"badPackets":{}}}"#,
769        counters.total_packets,
770        counters.recovered_packets,
771        counters.lost_packets,
772        counters.bad_packets
773    )
774}
775
776fn escape_json_str(value: &str) -> String {
777    let mut out = String::with_capacity(value.len());
778    for ch in value.chars() {
779        match ch {
780            '"' => out.push_str("\\\""),
781            '\\' => out.push_str("\\\\"),
782            '\n' => out.push_str("\\n"),
783            '\r' => out.push_str("\\r"),
784            '\t' => out.push_str("\\t"),
785            _ => out.push(ch),
786        }
787    }
788    out
789}
790
791fn ms_from_js(now_ms: f64) -> u64 {
792    if now_ms.is_finite() && now_ms > 0.0 {
793        now_ms.min(u64::MAX as f64) as u64
794    } else {
795        0
796    }
797}
798
799#[wasm_bindgen(js_name = supportedUsbFilters)]
800pub fn supported_usb_filters() -> String {
801    // Kept as JSON to avoid forcing web-sys types into the Rust API.
802    r#"[{"vendorId":3034,"productId":34834},{"vendorId":3034,"productId":2065},{"vendorId":3034,"productId":43025},{"vendorId":3034,"productId":47121},{"vendorId":3034,"productId":34835},{"vendorId":9047,"productId":288}]"#.to_owned()
803}
804
805#[cfg(target_arch = "wasm32")]
806#[wasm_bindgen(js_name = listAuthorizedUsbDevices)]
807pub async fn list_authorized_usb_devices() -> Result<Array, JsValue> {
808    let devices = nusb::list_devices()
809        .await
810        .map_err(|err| JsValue::from_str(&format!("nusb list_devices failed: {err}")))?;
811
812    let out = Array::new();
813    for device in devices {
814        let obj = Object::new();
815        Reflect::set(
816            &obj,
817            &JsValue::from_str("vendorId"),
818            &JsValue::from_f64(device.vendor_id() as f64),
819        )?;
820        Reflect::set(
821            &obj,
822            &JsValue::from_str("productId"),
823            &JsValue::from_f64(device.product_id() as f64),
824        )?;
825        if let Some(product) = device.product_string() {
826            Reflect::set(
827                &obj,
828                &JsValue::from_str("product"),
829                &JsValue::from_str(product),
830            )?;
831        }
832        if let Some(manufacturer) = device.manufacturer_string() {
833            Reflect::set(
834                &obj,
835                &JsValue::from_str("manufacturer"),
836                &JsValue::from_str(manufacturer),
837            )?;
838        }
839        out.push(&obj);
840    }
841    Ok(out)
842}
843
844fn parse_hex_u64(input: &str) -> Result<u64, JsValue> {
845    let trimmed = input.trim();
846    let hex = trimmed
847        .strip_prefix("0x")
848        .or_else(|| trimmed.strip_prefix("0X"))
849        .unwrap_or(trimmed);
850    u64::from_str_radix(hex, 16)
851        .map_err(|err| JsValue::from_str(&format!("invalid nonce hex: {err}")))
852}
853
854#[cfg(test)]
855mod tests {
856    use super::*;
857
858    #[test]
859    fn h264_codec_string_comes_from_annex_b_sps() {
860        let frame = [0, 0, 0, 1, 0x67, 0x64, 0x00, 0x1f, 0xac, 0xd9];
861        assert_eq!(h264_codec_string(&frame).as_deref(), Some("avc1.64001F"));
862    }
863}