Skip to main content

openipc_web/
mock.rs

1use js_sys::{Array, Object, Reflect, Uint8Array};
2use openipc_core::{
3    ChannelId, FrameLayout, MockRtpPipeline, PayloadRouteId, ReceiverBatch, ReceiverBatchOptions,
4    ReceiverRuntime,
5};
6use wasm_bindgen::prelude::*;
7
8use crate::js::{elapsed_ms, now_ms, raw_payload_object, set_number};
9use crate::video::{rtp_status_object, video_frame_object};
10
11const MOCK_VIDEO_ROUTE_ID: PayloadRouteId = PayloadRouteId::new(1);
12const MOCK_KEY_SLOT: u64 = 0;
13
14#[wasm_bindgen]
15/// No-hardware development source that generates RTP-backed RGBA test frames.
16pub struct OpenIpcMockRtpPipeline {
17    pipeline: MockRtpPipeline,
18}
19
20#[wasm_bindgen]
21impl OpenIpcMockRtpPipeline {
22    #[wasm_bindgen(constructor)]
23    /// Create a mock RTP pipeline.
24    pub fn new(width: u16, height: u16, fps: u16) -> OpenIpcMockRtpPipeline {
25        OpenIpcMockRtpPipeline {
26            pipeline: MockRtpPipeline::new(width, height, fps),
27        }
28    }
29
30    #[wasm_bindgen(js_name = nextFrame, unchecked_return_type = "OpenIpcMockFrame")]
31    /// Generate and recover the next mock RTP frame.
32    pub fn next_frame(&mut self) -> Result<Object, JsValue> {
33        let frame = self
34            .pipeline
35            .next_frame()
36            .map_err(|err| JsValue::from_str(&format!("mock RTP failed: {err:?}")))?;
37        let object = Object::new();
38        Reflect::set(
39            &object,
40            &JsValue::from_str("width"),
41            &JsValue::from_f64(f64::from(frame.width)),
42        )?;
43        Reflect::set(
44            &object,
45            &JsValue::from_str("height"),
46            &JsValue::from_f64(f64::from(frame.height)),
47        )?;
48        Reflect::set(
49            &object,
50            &JsValue::from_str("frameIndex"),
51            &JsValue::from_str(&frame.frame_index.to_string()),
52        )?;
53        Reflect::set(
54            &object,
55            &JsValue::from_str("timestamp"),
56            &JsValue::from_f64(f64::from(frame.timestamp)),
57        )?;
58        Reflect::set(
59            &object,
60            &JsValue::from_str("rtpPackets"),
61            &JsValue::from_f64(frame.rtp_packets as f64),
62        )?;
63        Reflect::set(
64            &object,
65            &JsValue::from_str("rtpBytes"),
66            &JsValue::from_f64(frame.rtp_bytes as f64),
67        )?;
68        Reflect::set(
69            &object,
70            &JsValue::from_str("rgba"),
71            &Uint8Array::from(frame.rgba.as_slice()),
72        )?;
73        Ok(object)
74    }
75}
76
77#[wasm_bindgen]
78/// No-hardware route runtime backed by the core mock payload pipeline.
79///
80/// JavaScript can feed synthetic recovered payload bytes, such as RTP packets,
81/// and receive the same profiled batch shape used by real RX transfers.
82pub struct OpenIpcMockPayloadRuntime {
83    runtime: ReceiverRuntime,
84    packet_seq: u64,
85}
86
87#[wasm_bindgen]
88impl OpenIpcMockPayloadRuntime {
89    #[wasm_bindgen(constructor)]
90    /// Create a mock payload runtime for one channel id.
91    pub fn new(channel_id: u32) -> OpenIpcMockPayloadRuntime {
92        OpenIpcMockPayloadRuntime {
93            runtime: ReceiverRuntime::with_mock_video_route(
94                FrameLayout::WithFcs,
95                MOCK_VIDEO_ROUTE_ID,
96                ChannelId::new(channel_id),
97                MOCK_KEY_SLOT,
98            ),
99            packet_seq: 0,
100        }
101    }
102
103    #[wasm_bindgen(js_name = setRtpReorderEnabled)]
104    /// Enable or disable the RTP reorder buffer used by the video route.
105    pub fn set_rtp_reorder_enabled(&mut self, enabled: bool) {
106        self.runtime.set_rtp_reorder_enabled(enabled);
107    }
108
109    #[wasm_bindgen(
110        js_name = pushPayloadProfiled,
111        unchecked_return_type = "OpenIpcRxTransferProfile"
112    )]
113    /// Push one synthetic recovered payload and return the standard RX profile.
114    pub fn push_payload_profiled(&mut self, payload: &[u8]) -> Result<Object, JsValue> {
115        let total_start = now_ms();
116        let pipeline_start = now_ms();
117        let batch = self
118            .runtime
119            .push_mock_payload(
120                self.runtime.video_runtime(),
121                self.packet_seq,
122                payload,
123                &ReceiverBatchOptions::default(),
124            )
125            .map_err(|err| JsValue::from_str(&format!("mock payload rejected: {err}")))?;
126        self.packet_seq = self.packet_seq.wrapping_add(1);
127        let pipeline_ms = elapsed_ms(pipeline_start);
128        profiled_batch_object(
129            batch,
130            payload.len(),
131            0.0,
132            pipeline_ms,
133            elapsed_ms(total_start),
134        )
135    }
136}
137
138fn profiled_batch_object(
139    batch: ReceiverBatch,
140    transfer_len: usize,
141    parse_ms: f64,
142    pipeline_ms: f64,
143    total_ms: f64,
144) -> Result<Object, JsValue> {
145    let counters = batch.counters;
146    let frames = frame_objects_array(batch.frames)?;
147    let raw_payloads = raw_payload_array(batch.raw_payloads)?;
148    let rtp_status = rtp_status_object(batch.rtp_status, batch.rtp_reorder_status)?;
149
150    let object = Object::new();
151    Reflect::set(&object, &JsValue::from_str("frames"), &frames)?;
152    Reflect::set(&object, &JsValue::from_str("rawPayloads"), &raw_payloads)?;
153    Reflect::set(
154        &object,
155        &JsValue::from_str("mavlinkPayloads"),
156        &raw_payloads,
157    )?;
158    Reflect::set(&object, &JsValue::from_str("rtpStatus"), &rtp_status)?;
159    set_number(
160        &object,
161        "rawPayloadCount",
162        counters.raw_payload_count as f64,
163    )?;
164    set_number(
165        &object,
166        "rawPayloadBytes",
167        counters.raw_payload_bytes as f64,
168    )?;
169    set_number(&object, "transferBytes", transfer_len as f64)?;
170    set_number(&object, "packets", counters.packets.max(1) as f64)?;
171    set_number(
172        &object,
173        "acceptedPackets",
174        counters.accepted_packets.max(1) as f64,
175    )?;
176    set_number(&object, "droppedPackets", counters.dropped_packets as f64)?;
177    set_number(&object, "crcDropped", counters.crc_dropped as f64)?;
178    set_number(&object, "icvDropped", counters.icv_dropped as f64)?;
179    set_number(&object, "reportDropped", counters.report_dropped as f64)?;
180    set_number(&object, "ignoredFrames", counters.ignored_frames as f64)?;
181    set_number(&object, "sessions", counters.sessions as f64)?;
182    set_number(&object, "wfbPayloads", counters.wfb_payloads as f64)?;
183    set_number(&object, "rtpPackets", counters.rtp_packets as f64)?;
184    set_number(&object, "videoFrames", counters.video_frames as f64)?;
185    set_number(
186        &object,
187        "mavlinkPayloadCount",
188        counters.raw_payload_count as f64,
189    )?;
190    set_number(&object, "mavlinkBytes", counters.raw_payload_bytes as f64)?;
191    set_number(&object, "parseMs", parse_ms)?;
192    set_number(&object, "pipelineMs", pipeline_ms)?;
193    set_number(&object, "totalMs", total_ms)?;
194    Ok(object)
195}
196
197fn frame_objects_array(frames: Vec<openipc_core::DepacketizedFrame>) -> Result<Array, JsValue> {
198    let out = Array::new();
199    for frame in frames {
200        out.push(&video_frame_object(frame)?.into());
201    }
202    Ok(out)
203}
204
205fn raw_payload_array(payloads: Vec<openipc_core::RoutePayload>) -> Result<Array, JsValue> {
206    let out = Array::new();
207    for payload in payloads {
208        out.push(&raw_payload_object(payload)?.into());
209    }
210    Ok(out)
211}