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 PayloadPipeline, PayloadPipelineEvent, PipelineEvent, RadioPort, ReceiverPipeline, WfbKeypair,
9 WfbTxKeypair,
10};
11#[cfg(target_arch = "wasm32")]
12use openipc_rtl88xx::{
13 ChannelWidth, DriverOptions, FalseAlarmCounters, Firmware8814Mode, InitReport, InitStatus,
14 IqkReport, MonitorOptions, PhydmDigState, PhydmWatchdogReport, PowerTrackingReport,
15 PowerTrackingState, RadioConfig, RealtekDevice, ThermalBucket,
16};
17use wasm_bindgen::prelude::*;
18
19#[wasm_bindgen(typescript_custom_section)]
20const OPENIPC_VIDEO_FRAME_TYPES: &'static str = r#"
21export type OpenIpcVideoFrame = {
22 data: Uint8Array;
23 codec: "h264" | "h265";
24 codecString: string;
25 isKeyFrame: boolean;
26 timestamp: number;
27};
28
29export type OpenIpcRawPayload = {
30 data: Uint8Array;
31 packetSeq: string;
32 channelId: number;
33};
34
35export type OpenIpcRxTransferProfile = {
36 frames: OpenIpcVideoFrame[];
37 mavlinkPayloads: OpenIpcRawPayload[];
38 transferBytes: number;
39 packets: number;
40 acceptedPackets: number;
41 droppedPackets: number;
42 crcDropped: number;
43 icvDropped: number;
44 reportDropped: number;
45 ignoredFrames: number;
46 sessions: number;
47 wfbPayloads: number;
48 rtpPackets: number;
49 videoFrames: number;
50 mavlinkPayloadCount: number;
51 mavlinkBytes: number;
52 parseMs: number;
53 pipelineMs: number;
54 totalMs: number;
55};
56"#;
57
58#[wasm_bindgen]
59pub struct OpenIpcReceiver {
60 pipeline: ReceiverPipeline,
61 mavlink_pipeline: Option<PayloadPipeline>,
62}
63
64#[wasm_bindgen]
65impl OpenIpcReceiver {
66 #[wasm_bindgen(constructor)]
67 pub fn new() -> Result<OpenIpcReceiver, JsValue> {
68 Self::with_channel_id(openipc_core::channel::DEFAULT_LINK_ID << 8, 1, 5)
69 }
70
71 #[wasm_bindgen(js_name = withChannelId)]
72 pub fn with_channel_id(
73 channel_id: u32,
74 fec_k: usize,
75 fec_n: usize,
76 ) -> Result<OpenIpcReceiver, JsValue> {
77 let pipeline = ReceiverPipeline::new(
78 ChannelId::new(channel_id),
79 FrameLayout::WithFcs,
80 fec_k,
81 fec_n,
82 )
83 .map_err(|err| JsValue::from_str(&format!("invalid receiver config: {err:?}")))?;
84 Ok(Self {
85 pipeline,
86 mavlink_pipeline: None,
87 })
88 }
89
90 #[wasm_bindgen(js_name = withKeypair)]
91 pub fn with_keypair(
92 channel_id: u32,
93 keypair: &[u8],
94 minimum_epoch: u64,
95 ) -> Result<OpenIpcReceiver, JsValue> {
96 let keypair = WfbKeypair::from_bytes(keypair)
97 .map_err(|err| JsValue::from_str(&format!("invalid WFB keypair: {err}")))?;
98 let mavlink_channel_id =
99 ChannelId::from_link_port(channel_id >> 8, RadioPort::MavlinkRx).raw();
100 openipc_receiver_with_keypair_and_mavlink_channel_inner(
101 channel_id,
102 mavlink_channel_id,
103 keypair,
104 minimum_epoch,
105 )
106 }
107
108 #[wasm_bindgen(js_name = withKeypairAndMavlinkChannel)]
109 pub fn with_keypair_and_mavlink_channel(
110 channel_id: u32,
111 mavlink_channel_id: u32,
112 keypair: &[u8],
113 minimum_epoch: u64,
114 ) -> Result<OpenIpcReceiver, JsValue> {
115 let keypair = WfbKeypair::from_bytes(keypair)
116 .map_err(|err| JsValue::from_str(&format!("invalid WFB keypair: {err}")))?;
117 openipc_receiver_with_keypair_and_mavlink_channel_inner(
118 channel_id,
119 mavlink_channel_id,
120 keypair,
121 minimum_epoch,
122 )
123 }
124
125 #[wasm_bindgen(js_name = pushRtpPacket)]
126 pub fn push_rtp_packet(&mut self, data: &[u8]) -> Option<Uint8Array> {
127 self.pipeline
128 .push_rtp(data)
129 .map(|frame| Uint8Array::from(frame.data.as_slice()))
130 }
131
132 #[wasm_bindgen(
133 js_name = pushRtpPacketDetailed,
134 unchecked_return_type = "OpenIpcVideoFrame | null"
135 )]
136 pub fn push_rtp_packet_detailed(&mut self, data: &[u8]) -> Result<JsValue, JsValue> {
137 match self.pipeline.push_rtp(data) {
138 Some(frame) => Ok(video_frame_object(frame)?.into()),
139 None => Ok(JsValue::NULL),
140 }
141 }
142
143 #[wasm_bindgen(js_name = pushDecryptedFragment)]
144 pub fn push_decrypted_fragment(
145 &mut self,
146 data_nonce_hex: &str,
147 fragment: &[u8],
148 ) -> Result<Array, JsValue> {
149 let data_nonce = parse_hex_u64(data_nonce_hex)?;
150 let events = self
151 .pipeline
152 .push_decrypted_fragment(data_nonce, fragment)
153 .map_err(|err| JsValue::from_str(&format!("WFB fragment rejected: {err:?}")))?;
154
155 let frames = Array::new();
156 for event in events {
157 if let PipelineEvent::VideoFrame(frame) = event {
158 frames.push(&Uint8Array::from(frame.data.as_slice()));
159 }
160 }
161 Ok(frames)
162 }
163
164 #[wasm_bindgen(js_name = pushDecrypted80211Frame)]
165 pub fn push_decrypted_80211_frame(
166 &mut self,
167 frame: &[u8],
168 fragment: &[u8],
169 ) -> Result<Array, JsValue> {
170 let events = self
171 .pipeline
172 .push_decrypted_80211_frame(frame, fragment)
173 .map_err(|err| JsValue::from_str(&format!("802.11 frame rejected: {err:?}")))?;
174 let frames = Array::new();
175 for event in events {
176 if let PipelineEvent::VideoFrame(frame) = event {
177 frames.push(&Uint8Array::from(frame.data.as_slice()));
178 }
179 }
180 Ok(frames)
181 }
182
183 #[wasm_bindgen(js_name = pushEncrypted80211Frame)]
184 pub fn push_encrypted_80211_frame(&mut self, frame: &[u8]) -> Result<Array, JsValue> {
185 let events = self
186 .pipeline
187 .push_80211_frame(frame)
188 .map_err(|err| JsValue::from_str(&format!("802.11 frame rejected: {err}")))?;
189 Ok(video_frames_from_events(events))
190 }
191
192 #[wasm_bindgen(js_name = pushRxTransfer)]
193 pub fn push_rx_transfer(&mut self, transfer: &[u8]) -> Result<Array, JsValue> {
194 let packets = parse_rx_aggregate(transfer)
195 .map_err(|err| JsValue::from_str(&format!("Realtek RX aggregate rejected: {err}")))?;
196 let frames = Array::new();
197 for packet in packets {
198 if packet.attrib.crc_err
199 || packet.attrib.icv_err
200 || packet.attrib.pkt_rpt_type != RxPacketType::NormalRx
201 {
202 continue;
203 }
204 let events = self
205 .pipeline
206 .push_80211_frame(packet.data)
207 .map_err(|err| JsValue::from_str(&format!("OpenIPC frame rejected: {err}")))?;
208 append_video_frames(&frames, events);
209 }
210 Ok(frames)
211 }
212
213 #[wasm_bindgen(
214 js_name = pushRxTransferDetailed,
215 unchecked_return_type = "OpenIpcVideoFrame[]"
216 )]
217 pub fn push_rx_transfer_detailed(&mut self, transfer: &[u8]) -> Result<Array, JsValue> {
218 self.push_rx_transfer_detailed_with_options(transfer, false)
219 }
220
221 #[wasm_bindgen(
222 js_name = pushRxTransferDetailedWithOptions,
223 unchecked_return_type = "OpenIpcVideoFrame[]"
224 )]
225 pub fn push_rx_transfer_detailed_with_options(
226 &mut self,
227 transfer: &[u8],
228 keep_corrupted: bool,
229 ) -> Result<Array, JsValue> {
230 let packets = parse_rx_aggregate(transfer)
231 .map_err(|err| JsValue::from_str(&format!("Realtek RX aggregate rejected: {err}")))?;
232 let frames = Array::new();
233 for packet in packets {
234 if !accept_rx_packet(packet.attrib, keep_corrupted) {
235 continue;
236 }
237 let events = self
238 .pipeline
239 .push_80211_frame(packet.data)
240 .map_err(|err| JsValue::from_str(&format!("OpenIPC frame rejected: {err}")))?;
241 append_video_frame_objects(&frames, events)?;
242 }
243 Ok(frames)
244 }
245
246 #[wasm_bindgen(
247 js_name = pushRxTransferProfiled,
248 unchecked_return_type = "OpenIpcRxTransferProfile"
249 )]
250 pub fn push_rx_transfer_profiled(&mut self, transfer: &[u8]) -> Result<Object, JsValue> {
251 self.push_rx_transfer_profiled_with_options(transfer, false)
252 }
253
254 #[wasm_bindgen(
255 js_name = pushRxTransferProfiledWithOptions,
256 unchecked_return_type = "OpenIpcRxTransferProfile"
257 )]
258 pub fn push_rx_transfer_profiled_with_options(
259 &mut self,
260 transfer: &[u8],
261 keep_corrupted: bool,
262 ) -> Result<Object, JsValue> {
263 let total_start = now_ms();
264 let parse_start = now_ms();
265 let packets = parse_rx_aggregate(transfer)
266 .map_err(|err| JsValue::from_str(&format!("Realtek RX aggregate rejected: {err}")))?;
267 let parse_ms = elapsed_ms(parse_start);
268
269 let frames = Array::new();
270 let mavlink_payloads = Array::new();
271 let mut accepted_packets = 0usize;
272 let mut crc_dropped = 0usize;
273 let mut icv_dropped = 0usize;
274 let mut report_dropped = 0usize;
275 let mut ignored_frames = 0usize;
276 let mut sessions = 0usize;
277 let mut wfb_payloads = 0usize;
278 let mut rtp_packets = 0usize;
279 let mut video_frames = 0usize;
280 let mut mavlink_payload_count = 0usize;
281 let mut mavlink_bytes = 0usize;
282
283 let pipeline_start = now_ms();
284 let packet_count = packets.len();
285 for packet in packets {
286 if packet.attrib.crc_err && !keep_corrupted {
287 crc_dropped += 1;
288 continue;
289 }
290 if packet.attrib.icv_err && !keep_corrupted {
291 icv_dropped += 1;
292 continue;
293 }
294 if packet.attrib.pkt_rpt_type != RxPacketType::NormalRx {
295 report_dropped += 1;
296 continue;
297 }
298 accepted_packets += 1;
299 if let Some(mavlink_pipeline) = self.mavlink_pipeline.as_mut() {
300 if let Ok(events) = mavlink_pipeline.push_80211_frame(packet.data) {
301 append_payload_objects(
302 &mavlink_payloads,
303 events,
304 mavlink_pipeline.channel_id(),
305 &mut mavlink_payload_count,
306 &mut mavlink_bytes,
307 )?;
308 }
309 }
310 let events = self
311 .pipeline
312 .push_80211_frame(packet.data)
313 .map_err(|err| JsValue::from_str(&format!("OpenIPC frame rejected: {err}")))?;
314 for event in events {
315 match event {
316 PipelineEvent::IgnoredFrame => ignored_frames += 1,
317 PipelineEvent::SessionEstablished { .. } => sessions += 1,
318 PipelineEvent::WfbPayload { .. } => wfb_payloads += 1,
319 PipelineEvent::RtpPacket { .. } => rtp_packets += 1,
320 PipelineEvent::VideoFrame(frame) => {
321 video_frames += 1;
322 frames.push(&video_frame_object(frame)?.into());
323 }
324 }
325 }
326 }
327 let pipeline_ms = elapsed_ms(pipeline_start);
328
329 let object = Object::new();
330 Reflect::set(&object, &JsValue::from_str("frames"), &frames)?;
331 Reflect::set(
332 &object,
333 &JsValue::from_str("mavlinkPayloads"),
334 &mavlink_payloads,
335 )?;
336 set_number(&object, "transferBytes", transfer.len() as f64)?;
337 set_number(&object, "packets", packet_count as f64)?;
338 set_number(&object, "acceptedPackets", accepted_packets as f64)?;
339 set_number(
340 &object,
341 "droppedPackets",
342 (crc_dropped + icv_dropped + report_dropped) as f64,
343 )?;
344 set_number(&object, "crcDropped", crc_dropped as f64)?;
345 set_number(&object, "icvDropped", icv_dropped as f64)?;
346 set_number(&object, "reportDropped", report_dropped as f64)?;
347 set_number(&object, "ignoredFrames", ignored_frames as f64)?;
348 set_number(&object, "sessions", sessions as f64)?;
349 set_number(&object, "wfbPayloads", wfb_payloads as f64)?;
350 set_number(&object, "rtpPackets", rtp_packets as f64)?;
351 set_number(&object, "videoFrames", video_frames as f64)?;
352 set_number(&object, "mavlinkPayloadCount", mavlink_payload_count as f64)?;
353 set_number(&object, "mavlinkBytes", mavlink_bytes as f64)?;
354 set_number(&object, "parseMs", parse_ms)?;
355 set_number(&object, "pipelineMs", pipeline_ms)?;
356 set_number(&object, "totalMs", elapsed_ms(total_start))?;
357 Ok(object)
358 }
359
360 #[wasm_bindgen(js_name = fecCounters)]
361 pub fn fec_counters(&self) -> String {
362 counters_json(self.pipeline.fec_counters())
363 }
364}
365
366#[wasm_bindgen]
367pub struct OpenIpcAdaptiveLink {
368 sender: AdaptiveLinkSender,
369 last_counters: FecCounters,
370 rx_channel_id: ChannelId,
371}
372
373#[wasm_bindgen]
374impl OpenIpcAdaptiveLink {
375 #[wasm_bindgen(constructor)]
376 pub fn new(
377 link_id: u32,
378 keypair: &[u8],
379 epoch: u64,
380 fec_k: usize,
381 fec_n: usize,
382 ) -> Result<OpenIpcAdaptiveLink, JsValue> {
383 let keypair = WfbTxKeypair::from_bytes(keypair)
384 .map_err(|err| JsValue::from_str(&format!("invalid adaptive-link keypair: {err}")))?;
385 let sender = AdaptiveLinkSender::new(link_id, keypair, epoch, fec_k, fec_n)
386 .map_err(|err| JsValue::from_str(&format!("invalid adaptive-link config: {err}")))?;
387 Ok(Self {
388 sender,
389 last_counters: FecCounters::default(),
390 rx_channel_id: ChannelId::from_link_port(link_id, RadioPort::Video),
391 })
392 }
393
394 #[wasm_bindgen(js_name = recordRx)]
395 pub fn record_rx(&mut self, now_ms: f64, rssi0: u8, rssi1: u8, snr0: i8, snr1: i8) {
396 self.sender
397 .link_mut()
398 .record_rx(ms_from_js(now_ms), rssi0, rssi1, snr0, snr1);
399 }
400
401 #[wasm_bindgen(js_name = recordRxTransfer)]
402 pub fn record_rx_transfer(&mut self, transfer: &[u8], now_ms: f64) -> Result<(), JsValue> {
403 let packets = parse_rx_aggregate(transfer)
404 .map_err(|err| JsValue::from_str(&format!("Realtek RX aggregate rejected: {err}")))?;
405 let now_ms = ms_from_js(now_ms);
406 for packet in packets {
407 if packet.attrib.crc_err
408 || packet.attrib.icv_err
409 || packet.attrib.pkt_rpt_type != RxPacketType::NormalRx
410 {
411 continue;
412 }
413 if !WifiFrame::parse(packet.data, FrameLayout::WithFcs)
414 .map(|frame| frame.matches_channel_id(self.rx_channel_id))
415 .unwrap_or(false)
416 {
417 continue;
418 }
419 self.sender
420 .record_rx_paths(now_ms, packet.attrib.rssi, packet.attrib.snr);
421 }
422 Ok(())
423 }
424
425 #[wasm_bindgen(js_name = recordReceiverCounters)]
426 pub fn record_receiver_counters(&mut self, receiver: &OpenIpcReceiver, now_ms: f64) {
427 self.record_counter_delta(ms_from_js(now_ms), receiver.pipeline.fec_counters());
428 }
429
430 #[wasm_bindgen(js_name = recordFec)]
431 pub fn record_fec(&mut self, now_ms: f64, total: u32, recovered: u32, lost: u32) {
432 self.sender
433 .record_fec(ms_from_js(now_ms), total, recovered, lost);
434 }
435
436 #[wasm_bindgen(js_name = requestKeyframe)]
437 pub fn request_keyframe(&mut self) {
438 self.sender.link_mut().request_keyframe();
439 }
440
441 #[wasm_bindgen(js_name = setKeyframeRequestMessages)]
442 pub fn set_keyframe_request_messages(&mut self, messages: u32) {
443 self.sender
444 .link_mut()
445 .set_keyframe_request_messages(messages);
446 }
447
448 #[wasm_bindgen(js_name = setVideoStartIdleMs)]
449 pub fn set_video_start_idle_ms(&mut self, idle_ms: u32) {
450 self.sender
451 .link_mut()
452 .set_video_start_idle_ms(idle_ms as u64);
453 }
454
455 #[wasm_bindgen(js_name = tick)]
456 pub fn tick(&mut self, now_ms: f64) -> Result<Array, JsValue> {
457 let frames = self
458 .sender
459 .tick(ms_from_js(now_ms))
460 .map_err(|err| JsValue::from_str(&format!("adaptive-link tick failed: {err}")))?;
461 let out = Array::new();
462 for frame in frames {
463 out.push(&Uint8Array::from(frame.as_slice()));
464 }
465 Ok(out)
466 }
467
468 #[wasm_bindgen(js_name = counters)]
469 pub fn counters(&self) -> String {
470 counters_json(self.last_counters)
471 }
472
473 #[wasm_bindgen(js_name = quality)]
474 pub fn quality(&mut self, now_ms: f64) -> String {
475 let quality = self.sender.link_mut().quality(ms_from_js(now_ms));
476 format!(
477 r#"{{"lostLastSecond":{},"recoveredLastSecond":{},"totalLastSecond":{},"rssi":[{},{}],"snr":[{},{}],"linkScore":[{},{}],"idrCode":"{}"}}"#,
478 quality.lost_last_second,
479 quality.recovered_last_second,
480 quality.total_last_second,
481 quality.rssi[0],
482 quality.rssi[1],
483 quality.snr[0],
484 quality.snr[1],
485 quality.link_score[0],
486 quality.link_score[1],
487 escape_json_str(&quality.idr_code),
488 )
489 }
490
491 fn record_counter_delta(&mut self, now_ms: u64, counters: FecCounters) {
492 let total = counters
493 .total_packets
494 .saturating_sub(self.last_counters.total_packets);
495 let recovered = counters
496 .recovered_packets
497 .saturating_sub(self.last_counters.recovered_packets);
498 let lost = counters
499 .lost_packets
500 .saturating_sub(self.last_counters.lost_packets);
501 self.last_counters = counters;
502 self.sender.record_fec(
503 now_ms,
504 total.min(u32::MAX as u64) as u32,
505 recovered.min(u32::MAX as u64) as u32,
506 lost.min(u32::MAX as u64) as u32,
507 );
508 }
509}
510
511#[cfg(target_arch = "wasm32")]
512#[wasm_bindgen]
513impl OpenIpcAdaptiveLink {
514 #[wasm_bindgen(js_name = tickAndSend)]
515 pub async fn tick_and_send(
516 &mut self,
517 device: &WebUsbRealtekDevice,
518 now_ms: f64,
519 current_channel: u8,
520 ) -> Result<usize, JsValue> {
521 let frames = self
522 .sender
523 .tick(ms_from_js(now_ms))
524 .map_err(|err| JsValue::from_str(&format!("adaptive-link tick failed: {err}")))?;
525 let count = frames.len();
526 for frame in frames {
527 device.send_packet(&frame, current_channel).await?;
528 }
529 Ok(count)
530 }
531}
532
533#[cfg(target_arch = "wasm32")]
534#[wasm_bindgen]
535pub struct WebUsbRealtekDevice {
536 driver: RealtekDevice,
537}
538
539#[cfg(target_arch = "wasm32")]
540#[wasm_bindgen]
541impl WebUsbRealtekDevice {
542 #[wasm_bindgen(js_name = fromWebUsbDevice)]
543 pub async fn from_web_usb_device(
544 device: web_sys::UsbDevice,
545 ) -> Result<WebUsbRealtekDevice, JsValue> {
546 let driver = RealtekDevice::from_web_usb_device(device)
547 .await
548 .map_err(driver_error)?;
549 Ok(Self { driver })
550 }
551
552 #[wasm_bindgen(js_name = fromWebUsbDeviceWithOptions)]
553 pub async fn from_web_usb_device_with_options(
554 device: web_sys::UsbDevice,
555 tx_endpoint_override: i32,
556 ) -> Result<WebUsbRealtekDevice, JsValue> {
557 Self::from_web_usb_device_advanced(device, tx_endpoint_override, -1, -1).await
558 }
559
560 #[wasm_bindgen(js_name = fromWebUsbDeviceAdvanced)]
561 pub async fn from_web_usb_device_advanced(
562 device: web_sys::UsbDevice,
563 tx_endpoint_override: i32,
564 target_vendor_id: i32,
565 target_product_id: i32,
566 ) -> Result<WebUsbRealtekDevice, JsValue> {
567 let driver = RealtekDevice::from_web_usb_device_with_options(
568 device,
569 DriverOptions {
570 tx_endpoint_override: optional_u8(tx_endpoint_override, "txEndpointOverride")?,
571 target_vendor_id: optional_u16(target_vendor_id, "targetVendorId")?,
572 target_product_id: optional_u16(target_product_id, "targetProductId")?,
573 ..DriverOptions::default()
574 },
575 )
576 .await
577 .map_err(driver_error)?;
578 Ok(Self { driver })
579 }
580
581 #[wasm_bindgen(js_name = bulkInEndpoint)]
582 pub fn bulk_in_endpoint(&self) -> u8 {
583 self.driver.bulk_in_ep
584 }
585
586 #[wasm_bindgen(js_name = bulkOutEndpoint)]
587 pub fn bulk_out_endpoint(&self) -> u8 {
588 self.driver.bulk_out_ep
589 }
590
591 #[wasm_bindgen(js_name = initializeMonitor)]
592 pub async fn initialize_monitor(
593 &self,
594 channel: u8,
595 channel_width_mhz: u16,
596 channel_offset: u8,
597 ) -> Result<String, JsValue> {
598 self.initialize_monitor_with_options(channel, channel_width_mhz, channel_offset, false)
599 .await
600 }
601
602 #[wasm_bindgen(js_name = initializeMonitorWithOptions)]
603 pub async fn initialize_monitor_with_options(
604 &self,
605 channel: u8,
606 channel_width_mhz: u16,
607 channel_offset: u8,
608 accept_bad_fcs: bool,
609 ) -> Result<String, JsValue> {
610 let radio = RadioConfig {
611 channel,
612 channel_offset,
613 channel_width: parse_channel_width(channel_width_mhz)?,
614 };
615 let report = self
616 .driver
617 .initialize_monitor_async(radio, accept_bad_fcs)
618 .await
619 .map_err(driver_error)?;
620 Ok(init_report_json(&report))
621 }
622
623 #[wasm_bindgen(js_name = initializeMonitorAdvanced)]
624 pub async fn initialize_monitor_advanced(
625 &self,
626 channel: u8,
627 channel_width_mhz: u16,
628 channel_offset: u8,
629 accept_bad_fcs: bool,
630 skip_tx_power: bool,
631 force_iqk: bool,
632 disable_iqk: bool,
633 firmware_8814_mode: String,
634 firmware_8814_chunk: i32,
635 ) -> Result<String, JsValue> {
636 let radio = RadioConfig {
637 channel,
638 channel_offset,
639 channel_width: parse_channel_width(channel_width_mhz)?,
640 };
641 let mode = if firmware_8814_mode.trim().is_empty() {
642 Firmware8814Mode::Kernel
643 } else {
644 Firmware8814Mode::from_env_value(&firmware_8814_mode).ok_or_else(|| {
645 JsValue::from_str("firmware8814Mode must be \"kernel\" or \"rtw88\"")
646 })?
647 };
648 let options = MonitorOptions {
649 accept_bad_fcs,
650 skip_tx_power,
651 force_iqk,
652 disable_iqk,
653 firmware_8814_mode: mode,
654 firmware_8814_chunk: optional_usize(firmware_8814_chunk, "firmware8814Chunk")?,
655 };
656 let report = self
657 .driver
658 .initialize_monitor_with_options_async(radio, options)
659 .await
660 .map_err(driver_error)?;
661 Ok(init_report_json(&report))
662 }
663
664 #[wasm_bindgen(js_name = readRxTransfer)]
665 pub async fn read_rx_transfer(&self, length: usize) -> Result<Uint8Array, JsValue> {
666 let bytes = self
667 .driver
668 .read_rx_transfer_async(length)
669 .await
670 .map_err(driver_error)?;
671 Ok(Uint8Array::from(bytes.as_slice()))
672 }
673
674 #[wasm_bindgen(js_name = readRxTransfers)]
675 pub async fn read_rx_transfers(
676 &self,
677 length: usize,
678 in_flight: usize,
679 ) -> Result<Array, JsValue> {
680 let transfers = self
681 .driver
682 .read_rx_transfers_async(length, in_flight)
683 .await
684 .map_err(driver_error)?;
685 let out = Array::new();
686 for transfer in transfers {
687 out.push(&Uint8Array::from(transfer.as_slice()));
688 }
689 Ok(out)
690 }
691
692 #[wasm_bindgen(js_name = writeTxTransfer)]
693 pub async fn write_tx_transfer(&self, transfer: &[u8]) -> Result<usize, JsValue> {
694 self.driver
695 .write_tx_transfer_async(transfer)
696 .await
697 .map_err(driver_error)
698 }
699
700 #[wasm_bindgen(js_name = sendPacket)]
701 pub async fn send_packet(
702 &self,
703 radiotap_packet: &[u8],
704 current_channel: u8,
705 ) -> Result<usize, JsValue> {
706 self.send_packet_with_options(radiotap_packet, current_channel, false)
707 .await
708 }
709
710 #[wasm_bindgen(js_name = sendPacketWithOptions)]
711 pub async fn send_packet_with_options(
712 &self,
713 radiotap_packet: &[u8],
714 current_channel: u8,
715 legacy_8812_descriptor: bool,
716 ) -> Result<usize, JsValue> {
717 let chip = self.driver.probe_chip_async().await.map_err(driver_error)?;
718 self.driver
719 .send_packet_async(
720 radiotap_packet,
721 RealtekTxOptions {
722 current_channel,
723 is_8814a: chip.family == openipc_rtl88xx::ChipFamily::Rtl8814,
724 legacy_8812_descriptor,
725 ..RealtekTxOptions::default()
726 },
727 )
728 .await
729 .map_err(driver_error)
730 }
731
732 #[wasm_bindgen(js_name = setTxPowerOverride)]
733 pub async fn set_tx_power_override(
734 &self,
735 current_channel: u8,
736 power: u8,
737 ) -> Result<(), JsValue> {
738 self.driver
739 .set_tx_power_override_async(current_channel, power)
740 .await
741 .map_err(driver_error)
742 }
743
744 #[wasm_bindgen(js_name = readThermalStatus)]
745 pub async fn read_thermal_status(&self) -> Result<String, JsValue> {
746 let status = self
747 .driver
748 .read_thermal_status_async()
749 .await
750 .map_err(driver_error)?;
751 Ok(format!(
752 r#"{{"raw":{},"baseline":{},"delta":{},"valid":{},"bucket":"{}"}}"#,
753 status.raw,
754 status.baseline,
755 status.delta,
756 status.valid,
757 thermal_bucket_name(status.bucket())
758 ))
759 }
760
761 #[wasm_bindgen(js_name = readQueueDepth8814)]
762 pub async fn read_queue_depth_8814(&self) -> Result<String, JsValue> {
763 let regs = self
764 .driver
765 .read_queue_depth_8814_async()
766 .await
767 .map_err(driver_error)?;
768 Ok(format!(
769 r#"[{},{},{},{},{}]"#,
770 regs[0], regs[1], regs[2], regs[3], regs[4]
771 ))
772 }
773
774 #[wasm_bindgen(js_name = readBbReg)]
775 pub async fn read_bb_reg(&self, register: u16, mask: u32) -> Result<u32, JsValue> {
776 self.driver
777 .read_bb_reg_async(register, mask)
778 .await
779 .map_err(driver_error)
780 }
781
782 #[wasm_bindgen(js_name = readBbDbgport)]
783 pub async fn read_bb_dbgport(&self, selector: u32) -> Result<String, JsValue> {
784 let read = self
785 .driver
786 .read_bb_dbgport_async(selector)
787 .await
788 .map_err(driver_error)?;
789 Ok(format!(
790 r#"{{"selector":{},"value":{},"savedSelector":{},"chipAlive":{}}}"#,
791 read.selector, read.value, read.saved_selector, read.chip_alive
792 ))
793 }
794
795 #[wasm_bindgen(js_name = readFalseAlarmCounters)]
796 pub async fn read_false_alarm_counters(&self) -> Result<String, JsValue> {
797 let counters = self
798 .driver
799 .read_false_alarm_counters_async()
800 .await
801 .map_err(driver_error)?;
802 Ok(false_alarm_counters_json(counters))
803 }
804
805 #[wasm_bindgen(js_name = runIqk)]
806 pub async fn run_iqk(&self, channel: u8) -> Result<String, JsValue> {
807 let chip = self.driver.probe_chip_async().await.map_err(driver_error)?;
808 let report = self
809 .driver
810 .run_iqk_async(chip, channel)
811 .await
812 .map_err(driver_error)?;
813 Ok(iqk_report_json(report))
814 }
815
816 #[wasm_bindgen(js_name = readRegisterU8)]
817 pub async fn read_register_u8(&self, register: u16) -> Result<u8, JsValue> {
818 self.driver
819 .read_u8_async(register)
820 .await
821 .map_err(driver_error)
822 }
823
824 #[wasm_bindgen(js_name = readRegisterU32)]
825 pub async fn read_register_u32(&self, register: u16) -> Result<u32, JsValue> {
826 self.driver
827 .read_u32_async(register)
828 .await
829 .map_err(driver_error)
830 }
831}
832
833#[cfg(target_arch = "wasm32")]
834#[wasm_bindgen]
835pub struct WebUsbPhydmWatchdog {
836 state: PhydmDigState,
837}
838
839#[cfg(target_arch = "wasm32")]
840#[wasm_bindgen]
841impl WebUsbPhydmWatchdog {
842 #[wasm_bindgen(constructor)]
843 pub fn new() -> Self {
844 Self {
845 state: PhydmDigState::default(),
846 }
847 }
848
849 #[wasm_bindgen(js_name = tick)]
850 pub async fn tick(&mut self, device: &WebUsbRealtekDevice) -> Result<String, JsValue> {
851 let report = device
852 .driver
853 .run_phydm_watchdog_tick_async(&mut self.state)
854 .await
855 .map_err(driver_error)?;
856 Ok(phydm_watchdog_report_json(report))
857 }
858}
859
860#[cfg(target_arch = "wasm32")]
861#[wasm_bindgen]
862pub struct WebUsbPowerTracking8812 {
863 state: PowerTrackingState,
864}
865
866#[cfg(target_arch = "wasm32")]
867#[wasm_bindgen]
868impl WebUsbPowerTracking8812 {
869 #[wasm_bindgen(constructor)]
870 pub fn new() -> Self {
871 Self {
872 state: PowerTrackingState::default(),
873 }
874 }
875
876 #[wasm_bindgen(js_name = init)]
877 pub async fn init(&mut self, device: &WebUsbRealtekDevice) -> Result<(), JsValue> {
878 device
879 .driver
880 .init_power_tracking_8812_async(&mut self.state)
881 .await
882 .map_err(driver_error)
883 }
884
885 #[wasm_bindgen(js_name = clear)]
886 pub async fn clear(&mut self, device: &WebUsbRealtekDevice) -> Result<(), JsValue> {
887 device
888 .driver
889 .clear_power_tracking_8812_async(&mut self.state)
890 .await
891 .map_err(driver_error)
892 }
893
894 #[wasm_bindgen(js_name = tick)]
895 pub async fn tick(
896 &mut self,
897 device: &WebUsbRealtekDevice,
898 channel: u8,
899 channel_width_mhz: u16,
900 ) -> Result<String, JsValue> {
901 let report = device
902 .driver
903 .tick_power_tracking_8812_async(
904 &mut self.state,
905 channel,
906 parse_channel_width(channel_width_mhz)?,
907 )
908 .await
909 .map_err(driver_error)?;
910 Ok(power_tracking_report_json(report))
911 }
912}
913
914#[cfg(target_arch = "wasm32")]
915fn thermal_bucket_name(bucket: ThermalBucket) -> &'static str {
916 match bucket {
917 ThermalBucket::Unknown => "unknown",
918 ThermalBucket::Cool => "cool",
919 ThermalBucket::Warm => "warm",
920 ThermalBucket::Hot => "hot",
921 ThermalBucket::Critical => "critical",
922 }
923}
924
925#[cfg(target_arch = "wasm32")]
926fn false_alarm_counters_json(counters: FalseAlarmCounters) -> String {
927 format!(
928 r#"{{"ofdmFail":{},"cckFail":{},"ofdmCca":{},"cckCca":{},"cckCrcOk":{},"cckCrcError":{},"ofdmCrcOk":{},"ofdmCrcError":{},"htCrcOk":{},"htCrcError":{},"vhtCrcOk":{},"vhtCrcError":{},"all":{},"ccaAll":{}}}"#,
929 counters.cnt_ofdm_fail,
930 counters.cnt_cck_fail,
931 counters.cnt_ofdm_cca,
932 counters.cnt_cck_cca,
933 counters.cnt_cck_crc32_ok,
934 counters.cnt_cck_crc32_error,
935 counters.cnt_ofdm_crc32_ok,
936 counters.cnt_ofdm_crc32_error,
937 counters.cnt_ht_crc32_ok,
938 counters.cnt_ht_crc32_error,
939 counters.cnt_vht_crc32_ok,
940 counters.cnt_vht_crc32_error,
941 counters.cnt_all,
942 counters.cnt_cca_all
943 )
944}
945
946#[cfg(target_arch = "wasm32")]
947fn phydm_watchdog_report_json(report: PhydmWatchdogReport) -> String {
948 format!(
949 r#"{{"previousIgi":{},"currentIgi":{},"counters":{}}}"#,
950 report.previous_igi,
951 report.current_igi,
952 false_alarm_counters_json(report.counters)
953 )
954}
955
956#[cfg(target_arch = "wasm32")]
957fn power_tracking_report_json(report: PowerTrackingReport) -> String {
958 format!(
959 r#"{{"enabled":{},"thermalRaw":{},"thermalAverage":{},"eepromThermal":{},"delta":{},"defaultOfdmIndex":{},"finalOfdmIndex":[{},{}],"swingDelta":[{},{}],"applied":{}}}"#,
960 report.enabled,
961 report.thermal_raw,
962 report.thermal_average,
963 report.eeprom_thermal,
964 report.delta,
965 report.default_ofdm_index,
966 report.final_ofdm_index[0],
967 report.final_ofdm_index[1],
968 report.swing_delta[0],
969 report.swing_delta[1],
970 report.applied
971 )
972}
973
974#[cfg(target_arch = "wasm32")]
975fn iqk_report_json(report: IqkReport) -> String {
976 format!(
977 r#"{{"chip":"{}","channel":{},"ran":{}}}"#,
978 report.chip.family.name(),
979 report.channel,
980 report.ran
981 )
982}
983
984#[cfg(target_arch = "wasm32")]
985fn parse_channel_width(width_mhz: u16) -> Result<ChannelWidth, JsValue> {
986 match width_mhz {
987 20 => Ok(ChannelWidth::Mhz20),
988 40 => Ok(ChannelWidth::Mhz40),
989 80 => Ok(ChannelWidth::Mhz80),
990 _ => Err(JsValue::from_str(
991 "unsupported channel width; expected 20, 40, or 80 MHz",
992 )),
993 }
994}
995
996#[cfg(target_arch = "wasm32")]
997fn optional_u8(value: i32, name: &str) -> Result<Option<u8>, JsValue> {
998 if value < 0 {
999 return Ok(None);
1000 }
1001 u8::try_from(value)
1002 .map(Some)
1003 .map_err(|_| JsValue::from_str(&format!("{name} is outside 0..255")))
1004}
1005
1006#[cfg(target_arch = "wasm32")]
1007fn optional_u16(value: i32, name: &str) -> Result<Option<u16>, JsValue> {
1008 if value < 0 {
1009 return Ok(None);
1010 }
1011 u16::try_from(value)
1012 .map(Some)
1013 .map_err(|_| JsValue::from_str(&format!("{name} is outside 0..65535")))
1014}
1015
1016#[cfg(target_arch = "wasm32")]
1017fn optional_usize(value: i32, name: &str) -> Result<Option<usize>, JsValue> {
1018 if value < 0 {
1019 return Ok(None);
1020 }
1021 usize::try_from(value)
1022 .map(Some)
1023 .map_err(|_| JsValue::from_str(&format!("{name} is invalid")))
1024}
1025
1026#[cfg(target_arch = "wasm32")]
1027fn init_report_json(report: &InitReport) -> String {
1028 let status = match report.status {
1029 InitStatus::AlreadyRunning => "already_running",
1030 InitStatus::Initialized => "initialized",
1031 };
1032 format!(
1033 r#"{{"chip":"{}","rfPaths":{},"cutVersion":{},"status":"{}","firmwareDownloaded":{}}}"#,
1034 report.chip.family.name(),
1035 report.chip.total_rf_paths(),
1036 report.chip.cut_version,
1037 status,
1038 report.firmware_downloaded
1039 )
1040}
1041
1042#[cfg(target_arch = "wasm32")]
1043fn driver_error(err: impl std::fmt::Display) -> JsValue {
1044 JsValue::from_str(&err.to_string())
1045}
1046
1047fn video_frames_from_events(events: Vec<PipelineEvent>) -> Array {
1048 let frames = Array::new();
1049 append_video_frames(&frames, events);
1050 frames
1051}
1052
1053fn append_video_frames(frames: &Array, events: Vec<PipelineEvent>) {
1054 for event in events {
1055 if let PipelineEvent::VideoFrame(frame) = event {
1056 frames.push(&Uint8Array::from(frame.data.as_slice()));
1057 }
1058 }
1059}
1060
1061fn append_video_frame_objects(frames: &Array, events: Vec<PipelineEvent>) -> Result<(), JsValue> {
1062 for event in events {
1063 if let PipelineEvent::VideoFrame(frame) = event {
1064 frames.push(&video_frame_object(frame)?.into());
1065 }
1066 }
1067 Ok(())
1068}
1069
1070fn append_payload_objects(
1071 payloads: &Array,
1072 events: Vec<PayloadPipelineEvent>,
1073 channel_id: ChannelId,
1074 payload_count: &mut usize,
1075 payload_bytes: &mut usize,
1076) -> Result<(), JsValue> {
1077 for event in events {
1078 if let PayloadPipelineEvent::Payload(payload) = event {
1079 *payload_count += 1;
1080 *payload_bytes += payload.data.len();
1081 payloads.push(&raw_payload_object(payload, channel_id)?.into());
1082 }
1083 }
1084 Ok(())
1085}
1086
1087fn raw_payload_object(
1088 payload: openipc_core::RecoveredPayload,
1089 channel_id: ChannelId,
1090) -> Result<Object, JsValue> {
1091 let object = Object::new();
1092 Reflect::set(
1093 &object,
1094 &JsValue::from_str("data"),
1095 &Uint8Array::from(payload.data.as_slice()),
1096 )?;
1097 Reflect::set(
1098 &object,
1099 &JsValue::from_str("packetSeq"),
1100 &JsValue::from_str(&payload.packet_seq.to_string()),
1101 )?;
1102 set_number(&object, "channelId", channel_id.raw() as f64)?;
1103 Ok(object)
1104}
1105
1106fn accept_rx_packet(attrib: openipc_core::realtek::RxPacketAttrib, keep_corrupted: bool) -> bool {
1107 attrib.pkt_rpt_type == RxPacketType::NormalRx
1108 && (keep_corrupted || (!attrib.crc_err && !attrib.icv_err))
1109}
1110
1111fn video_frame_object(frame: DepacketizedFrame) -> Result<Object, JsValue> {
1112 let object = Object::new();
1113 let codec_string = codec_string(&frame);
1114 Reflect::set(
1115 &object,
1116 &JsValue::from_str("data"),
1117 &Uint8Array::from(frame.data.as_slice()),
1118 )?;
1119 Reflect::set(
1120 &object,
1121 &JsValue::from_str("codec"),
1122 &JsValue::from_str(codec_name(frame.codec)),
1123 )?;
1124 Reflect::set(
1125 &object,
1126 &JsValue::from_str("codecString"),
1127 &JsValue::from_str(&codec_string),
1128 )?;
1129 Reflect::set(
1130 &object,
1131 &JsValue::from_str("isKeyFrame"),
1132 &JsValue::from_bool(frame.is_keyframe),
1133 )?;
1134 Reflect::set(
1135 &object,
1136 &JsValue::from_str("timestamp"),
1137 &JsValue::from_f64(f64::from(frame.timestamp)),
1138 )?;
1139 Ok(object)
1140}
1141
1142fn codec_name(codec: Codec) -> &'static str {
1143 match codec {
1144 Codec::H264 => "h264",
1145 Codec::H265 => "h265",
1146 }
1147}
1148
1149fn codec_string(frame: &DepacketizedFrame) -> String {
1150 match frame.codec {
1151 Codec::H264 => h264_codec_string(&frame.data).unwrap_or_else(|| "avc1.42E01E".to_owned()),
1152 Codec::H265 => "hev1.1.6.L93.B0".to_owned(),
1153 }
1154}
1155
1156fn h264_codec_string(frame: &[u8]) -> Option<String> {
1157 for unit in annex_b_units(frame) {
1158 let nalu = &frame[unit.start..unit.end];
1159 if nalu.len() >= 4 && nalu[0] & 0x1f == 7 {
1160 return Some(format!(
1161 "avc1.{}{}{}",
1162 hex_byte(nalu[1]),
1163 hex_byte(nalu[2]),
1164 hex_byte(nalu[3])
1165 ));
1166 }
1167 }
1168 None
1169}
1170
1171#[derive(Debug, Clone, Copy)]
1172struct AnnexBUnit {
1173 start: usize,
1174 end: usize,
1175}
1176
1177fn annex_b_units(frame: &[u8]) -> Vec<AnnexBUnit> {
1178 let mut starts = Vec::new();
1179 let mut index = 0;
1180 while index + 3 < frame.len() {
1181 let len = start_code_len(frame, index);
1182 if len > 0 {
1183 starts.push(index);
1184 index += len;
1185 } else {
1186 index += 1;
1187 }
1188 }
1189 if starts.is_empty() && !frame.is_empty() {
1190 return vec![AnnexBUnit {
1191 start: 0,
1192 end: frame.len(),
1193 }];
1194 }
1195 starts
1196 .iter()
1197 .enumerate()
1198 .map(|(index, start)| AnnexBUnit {
1199 start: start + start_code_len(frame, *start),
1200 end: starts.get(index + 1).copied().unwrap_or(frame.len()),
1201 })
1202 .collect()
1203}
1204
1205fn start_code_len(frame: &[u8], offset: usize) -> usize {
1206 if frame.get(offset) != Some(&0) || frame.get(offset + 1) != Some(&0) {
1207 return 0;
1208 }
1209 if frame.get(offset + 2) == Some(&1) {
1210 return 3;
1211 }
1212 if frame.get(offset + 2) == Some(&0) && frame.get(offset + 3) == Some(&1) {
1213 return 4;
1214 }
1215 0
1216}
1217
1218fn hex_byte(value: u8) -> String {
1219 format!("{value:02X}")
1220}
1221
1222fn set_number(object: &Object, key: &str, value: f64) -> Result<(), JsValue> {
1223 Reflect::set(object, &JsValue::from_str(key), &JsValue::from_f64(value))?;
1224 Ok(())
1225}
1226
1227fn now_ms() -> f64 {
1228 #[cfg(target_arch = "wasm32")]
1229 {
1230 web_sys::window()
1231 .and_then(|window| window.performance())
1232 .map(|performance| performance.now())
1233 .unwrap_or_else(js_sys::Date::now)
1234 }
1235 #[cfg(not(target_arch = "wasm32"))]
1236 {
1237 0.0
1238 }
1239}
1240
1241fn elapsed_ms(start_ms: f64) -> f64 {
1242 let elapsed = now_ms() - start_ms;
1243 if elapsed.is_finite() && elapsed >= 0.0 {
1244 elapsed
1245 } else {
1246 0.0
1247 }
1248}
1249
1250fn openipc_receiver_with_keypair_and_mavlink_channel_inner(
1251 channel_id: u32,
1252 mavlink_channel_id: u32,
1253 keypair: WfbKeypair,
1254 minimum_epoch: u64,
1255) -> Result<OpenIpcReceiver, JsValue> {
1256 let pipeline = ReceiverPipeline::with_keypair(
1257 ChannelId::new(channel_id),
1258 FrameLayout::WithFcs,
1259 keypair,
1260 minimum_epoch,
1261 )
1262 .map_err(|err| JsValue::from_str(&format!("invalid encrypted receiver config: {err}")))?;
1263 let mavlink_pipeline = PayloadPipeline::with_keypair(
1264 ChannelId::new(mavlink_channel_id),
1265 FrameLayout::WithFcs,
1266 keypair,
1267 minimum_epoch,
1268 )
1269 .map_err(|err| JsValue::from_str(&format!("invalid MAVLink receiver config: {err}")))?;
1270 Ok(OpenIpcReceiver {
1271 pipeline,
1272 mavlink_pipeline: Some(mavlink_pipeline),
1273 })
1274}
1275
1276fn counters_json(counters: FecCounters) -> String {
1277 format!(
1278 r#"{{"totalPackets":{},"recoveredPackets":{},"lostPackets":{},"badPackets":{}}}"#,
1279 counters.total_packets,
1280 counters.recovered_packets,
1281 counters.lost_packets,
1282 counters.bad_packets
1283 )
1284}
1285
1286fn escape_json_str(value: &str) -> String {
1287 let mut out = String::with_capacity(value.len());
1288 for ch in value.chars() {
1289 match ch {
1290 '"' => out.push_str("\\\""),
1291 '\\' => out.push_str("\\\\"),
1292 '\n' => out.push_str("\\n"),
1293 '\r' => out.push_str("\\r"),
1294 '\t' => out.push_str("\\t"),
1295 _ => out.push(ch),
1296 }
1297 }
1298 out
1299}
1300
1301fn ms_from_js(now_ms: f64) -> u64 {
1302 if now_ms.is_finite() && now_ms > 0.0 {
1303 now_ms.min(u64::MAX as f64) as u64
1304 } else {
1305 0
1306 }
1307}
1308
1309#[wasm_bindgen(js_name = supportedUsbFilters)]
1310pub fn supported_usb_filters() -> String {
1311 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()
1313}
1314
1315#[cfg(target_arch = "wasm32")]
1316#[wasm_bindgen(js_name = listAuthorizedUsbDevices)]
1317pub async fn list_authorized_usb_devices() -> Result<Array, JsValue> {
1318 let devices = nusb::list_devices()
1319 .await
1320 .map_err(|err| JsValue::from_str(&format!("nusb list_devices failed: {err}")))?;
1321
1322 let out = Array::new();
1323 for device in devices {
1324 let obj = Object::new();
1325 Reflect::set(
1326 &obj,
1327 &JsValue::from_str("vendorId"),
1328 &JsValue::from_f64(device.vendor_id() as f64),
1329 )?;
1330 Reflect::set(
1331 &obj,
1332 &JsValue::from_str("productId"),
1333 &JsValue::from_f64(device.product_id() as f64),
1334 )?;
1335 if let Some(product) = device.product_string() {
1336 Reflect::set(
1337 &obj,
1338 &JsValue::from_str("product"),
1339 &JsValue::from_str(product),
1340 )?;
1341 }
1342 if let Some(manufacturer) = device.manufacturer_string() {
1343 Reflect::set(
1344 &obj,
1345 &JsValue::from_str("manufacturer"),
1346 &JsValue::from_str(manufacturer),
1347 )?;
1348 }
1349 out.push(&obj);
1350 }
1351 Ok(out)
1352}
1353
1354fn parse_hex_u64(input: &str) -> Result<u64, JsValue> {
1355 let trimmed = input.trim();
1356 let hex = trimmed
1357 .strip_prefix("0x")
1358 .or_else(|| trimmed.strip_prefix("0X"))
1359 .unwrap_or(trimmed);
1360 u64::from_str_radix(hex, 16)
1361 .map_err(|err| JsValue::from_str(&format!("invalid nonce hex: {err}")))
1362}
1363
1364#[cfg(test)]
1365mod tests {
1366 use super::*;
1367
1368 #[test]
1369 fn h264_codec_string_comes_from_annex_b_sps() {
1370 let frame = [0, 0, 0, 1, 0x67, 0x64, 0x00, 0x1f, 0xac, 0xd9];
1371 assert_eq!(h264_codec_string(&frame).as_deref(), Some("avc1.64001F"));
1372 }
1373}