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 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}