@openipc-rs/web 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 openipc-rs contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,190 @@
1
+ # @openipc-rs/web
2
+
3
+ WebAssembly and WebUSB bindings for the `openipc-rs` receiver stack.
4
+
5
+ This package is the browser SDK layer. It exposes the Rust OpenIPC transport
6
+ pipeline to JavaScript:
7
+
8
+ - Realtek USB RX transfer parsing
9
+ - WFB session/decryption/FEC handling
10
+ - RTP depacketization
11
+ - H.264/H.265 Annex-B frame output for WebCodecs
12
+ - Adaptive-link feedback helpers
13
+ - WebUSB Realtek device access
14
+
15
+ It does not include a UI or video renderer. Applications are expected to feed
16
+ the encoded frames into WebCodecs, MSE, a worker pipeline, or another renderer.
17
+
18
+ ## Install
19
+
20
+ ```sh
21
+ npm install @openipc-rs/web
22
+ ```
23
+
24
+ ## Basic Shape
25
+
26
+ ```ts
27
+ import init, {
28
+ OpenIpcReceiver,
29
+ WebUsbRealtekDevice,
30
+ supportedUsbFilters,
31
+ } from "@openipc-rs/web";
32
+
33
+ await init();
34
+
35
+ const filters = JSON.parse(supportedUsbFilters());
36
+ const usbDevice = await navigator.usb.requestDevice({ filters });
37
+ const radio = await WebUsbRealtekDevice.fromWebUsbDevice(usbDevice);
38
+ const receiver = OpenIpcReceiver.withKeypair(channelId, keypairBytes, minimumEpoch);
39
+
40
+ await radio.initializeMonitor(channel, channelWidthMhz, channelOffset);
41
+
42
+ while (running) {
43
+ const transfer = await radio.readRxTransfer(32768);
44
+ const batch = receiver.pushRxTransferProfiled(transfer);
45
+ for (const frame of batch.frames) {
46
+ // frame.data is encoded H.264/H.265 Annex-B data.
47
+ // Feed it into WebCodecs as an EncodedVideoChunk.
48
+ }
49
+ }
50
+ ```
51
+
52
+ ## WebCodecs Rendering
53
+
54
+ The Rust/WASM side outputs compressed H.264/H.265 frames. Your app should pass
55
+ those frames to WebCodecs and render the decoded `VideoFrame` objects.
56
+
57
+ ```html
58
+ <canvas id="video"></canvas>
59
+ ```
60
+
61
+ ```ts
62
+ import init, {
63
+ OpenIpcReceiver,
64
+ WebUsbRealtekDevice,
65
+ supportedUsbFilters,
66
+ type OpenIpcVideoFrame,
67
+ } from "@openipc-rs/web";
68
+
69
+ const canvas = document.querySelector<HTMLCanvasElement>("#video")!;
70
+ const ctx = canvas.getContext("2d", { alpha: false })!;
71
+
72
+ let decoder: VideoDecoder | undefined;
73
+ let decoderKey = "";
74
+ let waitingForKeyframe = true;
75
+ let baseRtpTimestamp: number | undefined;
76
+ let baseTimestampUs = 0;
77
+
78
+ function timestampUs(rtpTimestamp: number): number {
79
+ if (baseRtpTimestamp === undefined) {
80
+ baseRtpTimestamp = rtpTimestamp >>> 0;
81
+ baseTimestampUs = Math.round(performance.now() * 1000);
82
+ }
83
+
84
+ const delta = (rtpTimestamp >>> 0) - baseRtpTimestamp;
85
+ return baseTimestampUs + Math.round((delta * 1_000_000) / 90_000);
86
+ }
87
+
88
+ function renderFrame(frame: VideoFrame) {
89
+ try {
90
+ const width = frame.displayWidth || frame.codedWidth;
91
+ const height = frame.displayHeight || frame.codedHeight;
92
+ if (canvas.width !== width || canvas.height !== height) {
93
+ canvas.width = width;
94
+ canvas.height = height;
95
+ }
96
+ ctx.drawImage(frame, 0, 0, width, height);
97
+ } finally {
98
+ frame.close();
99
+ }
100
+ }
101
+
102
+ async function ensureDecoder(frame: OpenIpcVideoFrame): Promise<boolean> {
103
+ const codec = frame.codecString;
104
+ const key = `${frame.codec}:${codec}`;
105
+ if (decoder && decoderKey === key) {
106
+ return true;
107
+ }
108
+
109
+ const config: VideoDecoderConfig =
110
+ frame.codec === "h264"
111
+ ? { codec, avc: { format: "annexb" }, hardwareAcceleration: "prefer-hardware", optimizeForLatency: true }
112
+ : { codec, hevc: { format: "annexb" }, hardwareAcceleration: "prefer-hardware", optimizeForLatency: true };
113
+
114
+ const support = await VideoDecoder.isConfigSupported(config);
115
+ if (!support.supported) {
116
+ return false;
117
+ }
118
+
119
+ decoder?.close();
120
+ decoder = new VideoDecoder({
121
+ output: renderFrame,
122
+ error: (error) => {
123
+ console.warn("VideoDecoder error", error);
124
+ waitingForKeyframe = true;
125
+ },
126
+ });
127
+ decoder.configure(support.config ?? config);
128
+ decoderKey = key;
129
+ waitingForKeyframe = true;
130
+ return true;
131
+ }
132
+
133
+ async function decodeOpenIpcFrame(frame: OpenIpcVideoFrame) {
134
+ if (!(await ensureDecoder(frame))) {
135
+ return;
136
+ }
137
+ if (waitingForKeyframe && !frame.isKeyFrame) {
138
+ return;
139
+ }
140
+
141
+ waitingForKeyframe = false;
142
+ decoder!.decode(
143
+ new EncodedVideoChunk({
144
+ type: frame.isKeyFrame ? "key" : "delta",
145
+ timestamp: timestampUs(frame.timestamp),
146
+ data: frame.data,
147
+ }),
148
+ );
149
+ }
150
+
151
+ await init();
152
+
153
+ const filters = JSON.parse(supportedUsbFilters());
154
+ const usbDevice = await navigator.usb.requestDevice({ filters });
155
+ const radio = await WebUsbRealtekDevice.fromWebUsbDevice(usbDevice);
156
+ const receiver = OpenIpcReceiver.withKeypair(channelId, keypairBytes, minimumEpoch);
157
+
158
+ await radio.initializeMonitor(channel, channelWidthMhz, channelOffset);
159
+
160
+ while (running) {
161
+ const transfer = await radio.readRxTransfer(32768);
162
+ const batch = receiver.pushRxTransferProfiled(transfer);
163
+ for (const frame of batch.frames) {
164
+ await decodeOpenIpcFrame(frame);
165
+ }
166
+ }
167
+ ```
168
+
169
+ ## Browser Requirements
170
+
171
+ - HTTPS or localhost secure context
172
+ - WebUSB support
173
+ - WebCodecs support for playback in typical browser apps
174
+ - A supported Realtek 802.11ac USB adapter
175
+
176
+ ## Build From Source
177
+
178
+ From the repository root:
179
+
180
+ ```sh
181
+ npm --prefix crates/openipc-web run build
182
+ ```
183
+
184
+ The build generates the publishable package in:
185
+
186
+ ```text
187
+ crates/openipc-web/pkg
188
+ ```
189
+
190
+ Generated files are not committed to Git.
@@ -0,0 +1,168 @@
1
+ /* tslint:disable */
2
+ /* eslint-disable */
3
+
4
+ export type OpenIpcVideoFrame = {
5
+ data: Uint8Array;
6
+ codec: "h264" | "h265";
7
+ codecString: string;
8
+ isKeyFrame: boolean;
9
+ timestamp: number;
10
+ };
11
+
12
+ export type OpenIpcRxTransferProfile = {
13
+ frames: OpenIpcVideoFrame[];
14
+ transferBytes: number;
15
+ packets: number;
16
+ acceptedPackets: number;
17
+ droppedPackets: number;
18
+ crcDropped: number;
19
+ icvDropped: number;
20
+ reportDropped: number;
21
+ ignoredFrames: number;
22
+ sessions: number;
23
+ wfbPayloads: number;
24
+ rtpPackets: number;
25
+ videoFrames: number;
26
+ parseMs: number;
27
+ pipelineMs: number;
28
+ totalMs: number;
29
+ };
30
+
31
+
32
+
33
+ export class OpenIpcAdaptiveLink {
34
+ free(): void;
35
+ [Symbol.dispose](): void;
36
+ counters(): string;
37
+ constructor(link_id: number, keypair: Uint8Array, epoch: bigint, fec_k: number, fec_n: number);
38
+ quality(now_ms: number): string;
39
+ recordFec(now_ms: number, total: number, recovered: number, lost: number): void;
40
+ recordReceiverCounters(receiver: OpenIpcReceiver, now_ms: number): void;
41
+ recordRx(now_ms: number, rssi0: number, rssi1: number, snr0: number, snr1: number): void;
42
+ recordRxTransfer(transfer: Uint8Array, now_ms: number): void;
43
+ requestKeyframe(): void;
44
+ setKeyframeRequestMessages(messages: number): void;
45
+ setVideoStartIdleMs(idle_ms: number): void;
46
+ tick(now_ms: number): Array<any>;
47
+ tickAndSend(device: WebUsbRealtekDevice, now_ms: number, current_channel: number): Promise<number>;
48
+ }
49
+
50
+ export class OpenIpcReceiver {
51
+ free(): void;
52
+ [Symbol.dispose](): void;
53
+ fecCounters(): string;
54
+ constructor();
55
+ pushDecrypted80211Frame(frame: Uint8Array, fragment: Uint8Array): Array<any>;
56
+ pushDecryptedFragment(data_nonce_hex: string, fragment: Uint8Array): Array<any>;
57
+ pushEncrypted80211Frame(frame: Uint8Array): Array<any>;
58
+ pushRtpPacket(data: Uint8Array): Uint8Array | undefined;
59
+ pushRtpPacketDetailed(data: Uint8Array): OpenIpcVideoFrame | null;
60
+ pushRxTransfer(transfer: Uint8Array): Array<any>;
61
+ pushRxTransferDetailed(transfer: Uint8Array): OpenIpcVideoFrame[];
62
+ pushRxTransferProfiled(transfer: Uint8Array): OpenIpcRxTransferProfile;
63
+ static withChannelId(channel_id: number, fec_k: number, fec_n: number): OpenIpcReceiver;
64
+ static withKeypair(channel_id: number, keypair: Uint8Array, minimum_epoch: bigint): OpenIpcReceiver;
65
+ }
66
+
67
+ export class WebUsbRealtekDevice {
68
+ private constructor();
69
+ free(): void;
70
+ [Symbol.dispose](): void;
71
+ bulkInEndpoint(): number;
72
+ bulkOutEndpoint(): number;
73
+ static fromWebUsbDevice(device: USBDevice): Promise<WebUsbRealtekDevice>;
74
+ initializeMonitor(channel: number, channel_width_mhz: number, channel_offset: number): Promise<string>;
75
+ readRegisterU32(register: number): Promise<number>;
76
+ readRegisterU8(register: number): Promise<number>;
77
+ readRxTransfer(length: number): Promise<Uint8Array>;
78
+ sendPacket(radiotap_packet: Uint8Array, current_channel: number): Promise<number>;
79
+ setTxPowerOverride(current_channel: number, power: number): Promise<void>;
80
+ writeTxTransfer(transfer: Uint8Array): Promise<number>;
81
+ }
82
+
83
+ export function listAuthorizedUsbDevices(): Promise<Array<any>>;
84
+
85
+ export function supportedUsbFilters(): string;
86
+
87
+ export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
88
+
89
+ export interface InitOutput {
90
+ readonly memory: WebAssembly.Memory;
91
+ readonly __wbg_openipcadaptivelink_free: (a: number, b: number) => void;
92
+ readonly __wbg_openipcreceiver_free: (a: number, b: number) => void;
93
+ readonly __wbg_webusbrealtekdevice_free: (a: number, b: number) => void;
94
+ readonly listAuthorizedUsbDevices: () => any;
95
+ readonly openipcadaptivelink_counters: (a: number) => [number, number];
96
+ readonly openipcadaptivelink_new: (a: number, b: number, c: number, d: bigint, e: number, f: number) => [number, number, number];
97
+ readonly openipcadaptivelink_quality: (a: number, b: number) => [number, number];
98
+ readonly openipcadaptivelink_recordFec: (a: number, b: number, c: number, d: number, e: number) => void;
99
+ readonly openipcadaptivelink_recordReceiverCounters: (a: number, b: number, c: number) => void;
100
+ readonly openipcadaptivelink_recordRx: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
101
+ readonly openipcadaptivelink_recordRxTransfer: (a: number, b: number, c: number, d: number) => [number, number];
102
+ readonly openipcadaptivelink_requestKeyframe: (a: number) => void;
103
+ readonly openipcadaptivelink_setKeyframeRequestMessages: (a: number, b: number) => void;
104
+ readonly openipcadaptivelink_setVideoStartIdleMs: (a: number, b: number) => void;
105
+ readonly openipcadaptivelink_tick: (a: number, b: number) => [number, number, number];
106
+ readonly openipcadaptivelink_tickAndSend: (a: number, b: number, c: number, d: number) => any;
107
+ readonly openipcreceiver_fecCounters: (a: number) => [number, number];
108
+ readonly openipcreceiver_new: () => [number, number, number];
109
+ readonly openipcreceiver_pushDecrypted80211Frame: (a: number, b: number, c: number, d: number, e: number) => [number, number, number];
110
+ readonly openipcreceiver_pushDecryptedFragment: (a: number, b: number, c: number, d: number, e: number) => [number, number, number];
111
+ readonly openipcreceiver_pushEncrypted80211Frame: (a: number, b: number, c: number) => [number, number, number];
112
+ readonly openipcreceiver_pushRtpPacket: (a: number, b: number, c: number) => any;
113
+ readonly openipcreceiver_pushRtpPacketDetailed: (a: number, b: number, c: number) => [number, number, number];
114
+ readonly openipcreceiver_pushRxTransfer: (a: number, b: number, c: number) => [number, number, number];
115
+ readonly openipcreceiver_pushRxTransferDetailed: (a: number, b: number, c: number) => [number, number, number];
116
+ readonly openipcreceiver_pushRxTransferProfiled: (a: number, b: number, c: number) => [number, number, number];
117
+ readonly openipcreceiver_withChannelId: (a: number, b: number, c: number) => [number, number, number];
118
+ readonly openipcreceiver_withKeypair: (a: number, b: number, c: number, d: bigint) => [number, number, number];
119
+ readonly supportedUsbFilters: () => [number, number];
120
+ readonly webusbrealtekdevice_bulkInEndpoint: (a: number) => number;
121
+ readonly webusbrealtekdevice_bulkOutEndpoint: (a: number) => number;
122
+ readonly webusbrealtekdevice_fromWebUsbDevice: (a: any) => any;
123
+ readonly webusbrealtekdevice_initializeMonitor: (a: number, b: number, c: number, d: number) => any;
124
+ readonly webusbrealtekdevice_readRegisterU32: (a: number, b: number) => any;
125
+ readonly webusbrealtekdevice_readRegisterU8: (a: number, b: number) => any;
126
+ readonly webusbrealtekdevice_readRxTransfer: (a: number, b: number) => any;
127
+ readonly webusbrealtekdevice_sendPacket: (a: number, b: number, c: number, d: number) => any;
128
+ readonly webusbrealtekdevice_setTxPowerOverride: (a: number, b: number, c: number) => any;
129
+ readonly webusbrealtekdevice_writeTxTransfer: (a: number, b: number, c: number) => any;
130
+ readonly wasm_bindgen__convert__closures_____invoke__h39fee390da28f45f: (a: number, b: number, c: any) => [number, number];
131
+ readonly wasm_bindgen__convert__closures_____invoke__h35992852bc42cc91: (a: number, b: number, c: any) => [number, number];
132
+ readonly wasm_bindgen__convert__closures_____invoke__h22e0fe300bd7dd70: (a: number, b: number, c: any) => [number, number];
133
+ readonly wasm_bindgen__convert__closures_____invoke__h35992852bc42cc91_3: (a: number, b: number, c: any) => [number, number];
134
+ readonly wasm_bindgen__convert__closures_____invoke__h35992852bc42cc91_4: (a: number, b: number, c: any) => [number, number];
135
+ readonly wasm_bindgen__convert__closures_____invoke__h32085c8bafd70877: (a: number, b: number, c: any, d: any) => void;
136
+ readonly wasm_bindgen__convert__closures_____invoke__h4d860cdb1981635b: (a: number, b: number) => void;
137
+ readonly __wbindgen_malloc: (a: number, b: number) => number;
138
+ readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
139
+ readonly __wbindgen_exn_store: (a: number) => void;
140
+ readonly __externref_table_alloc: () => number;
141
+ readonly __wbindgen_externrefs: WebAssembly.Table;
142
+ readonly __wbindgen_destroy_closure: (a: number, b: number) => void;
143
+ readonly __wbindgen_free: (a: number, b: number, c: number) => void;
144
+ readonly __externref_table_dealloc: (a: number) => void;
145
+ readonly __wbindgen_start: () => void;
146
+ }
147
+
148
+ export type SyncInitInput = BufferSource | WebAssembly.Module;
149
+
150
+ /**
151
+ * Instantiates the given `module`, which can either be bytes or
152
+ * a precompiled `WebAssembly.Module`.
153
+ *
154
+ * @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
155
+ *
156
+ * @returns {InitOutput}
157
+ */
158
+ export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
159
+
160
+ /**
161
+ * If `module_or_path` is {RequestInfo} or {URL}, makes a request and
162
+ * for everything else, calls `WebAssembly.instantiate` directly.
163
+ *
164
+ * @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
165
+ *
166
+ * @returns {Promise<InitOutput>}
167
+ */
168
+ export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;