@matbee/remotemedia-native 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/package.json ADDED
@@ -0,0 +1,106 @@
1
+ {
2
+ "name": "@matbee/remotemedia-native",
3
+ "version": "0.1.0",
4
+ "description": "Native Node.js bindings for RemoteMedia zero-copy IPC with iceoryx2 shared memory",
5
+ "main": "index.js",
6
+ "types": "index.d.ts",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/remotemedia/remotemedia-sdk.git",
10
+ "directory": "transports/ffi/nodejs"
11
+ },
12
+ "homepage": "https://github.com/remotemedia/remotemedia-sdk#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/remotemedia/remotemedia-sdk/issues"
15
+ },
16
+ "author": "RemoteMedia SDK Contributors",
17
+ "license": "MIT",
18
+ "keywords": [
19
+ "remotemedia",
20
+ "ipc",
21
+ "zero-copy",
22
+ "shared-memory",
23
+ "iceoryx2",
24
+ "napi-rs",
25
+ "native",
26
+ "ffi",
27
+ "pipeline",
28
+ "streaming",
29
+ "audio",
30
+ "video",
31
+ "machine-learning"
32
+ ],
33
+ "engines": {
34
+ "node": ">= 18"
35
+ },
36
+ "os": [
37
+ "darwin",
38
+ "linux"
39
+ ],
40
+ "cpu": [
41
+ "x64",
42
+ "arm64"
43
+ ],
44
+ "napi": {
45
+ "name": "remotemedia-native",
46
+ "triples": {
47
+ "additional": [
48
+ "aarch64-apple-darwin",
49
+ "aarch64-unknown-linux-gnu",
50
+ "x86_64-apple-darwin",
51
+ "x86_64-unknown-linux-gnu"
52
+ ]
53
+ }
54
+ },
55
+ "publishConfig": {
56
+ "access": "public",
57
+ "registry": "https://registry.npmjs.org/"
58
+ },
59
+ "scripts": {
60
+ "build": "../scripts/build-npm.sh --release",
61
+ "build:debug": "../scripts/build-npm.sh --debug",
62
+ "build:cargo": "cd ../../.. && cargo build --release --features napi --no-default-features -p remotemedia-ffi && cp target/release/libremotemedia_ffi.so target/release/remotemedia_native.node || cp target/release/libremotemedia_ffi.dylib target/release/remotemedia_native.node 2>/dev/null || true",
63
+ "build:cargo:debug": "cd ../../.. && rm -f target/release/remotemedia_native.node && cargo build --features napi --no-default-features -p remotemedia-ffi && cp target/debug/libremotemedia_ffi.so target/debug/remotemedia_native.node || cp target/debug/libremotemedia_ffi.dylib target/debug/remotemedia_native.node 2>/dev/null || true",
64
+ "build:napi": "napi build --platform --release",
65
+ "build:napi:debug": "napi build --platform",
66
+ "build:all": "../scripts/build-npm.sh --release --all",
67
+ "build:webrtc": "../scripts/build-npm.sh --release --webrtc",
68
+ "generate-types": "node scripts/generate-types.js",
69
+ "postbuild": "npm run generate-types || true",
70
+ "prepublishOnly": "test -n \"$(ls *.node 2>/dev/null)\" || (echo 'Error: No .node binaries found. Run npm run build first.' && exit 1)",
71
+ "publish:npm": "../scripts/publish-npm.sh",
72
+ "publish:npm:dry": "../scripts/publish-npm.sh --dry-run",
73
+ "test": "jest",
74
+ "test:subscriber": "jest subscriber.test.ts",
75
+ "test:publisher": "jest publisher.test.ts",
76
+ "test:session": "jest session.test.ts",
77
+ "test:webrtc": "jest webrtc.test.ts",
78
+ "test:webrtc-pipeline": "jest webrtc-pipeline.test.ts",
79
+ "benchmark": "node benchmark-zero-copy.js",
80
+ "clean": "rm -rf *.node node_modules"
81
+ },
82
+ "devDependencies": {
83
+ "@napi-rs/cli": "^2.18.4",
84
+ "@types/jest": "^29.5.0",
85
+ "@types/node": "^20.0.0",
86
+ "@types/ws": "^8.5.0",
87
+ "jest": "^29.5.0",
88
+ "ts-jest": "^29.1.0",
89
+ "typescript": "^5.0.0",
90
+ "werift": "^0.22.2"
91
+ },
92
+ "dependencies": {
93
+ "ws": "^8.18.0"
94
+ },
95
+ "files": [
96
+ "index.js",
97
+ "index.d.ts",
98
+ "proto-utils.js",
99
+ "proto-utils.d.ts",
100
+ "node-schemas.ts",
101
+ "node-schemas.json",
102
+ "*.node",
103
+ "LICENSE",
104
+ "README.md"
105
+ ]
106
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Protobuf DataBuffer Encoder/Decoder Utilities
3
+ *
4
+ * Browser and Node.js compatible utilities for encoding/decoding
5
+ * DataBuffer protobuf messages for WebRTC data channel communication.
6
+ */
7
+
8
+ // =============================================================================
9
+ // Decoded Types
10
+ // =============================================================================
11
+
12
+ export interface DecodedTextBuffer {
13
+ type: 'text';
14
+ textData: string;
15
+ encoding: string;
16
+ }
17
+
18
+ export interface DecodedJsonBuffer {
19
+ type: 'json';
20
+ jsonPayload: string;
21
+ schemaType?: string;
22
+ }
23
+
24
+ export interface DecodedAudioBuffer {
25
+ type: 'audio';
26
+ samples: Float32Array;
27
+ sampleRate: number;
28
+ channels: number;
29
+ }
30
+
31
+ export interface DecodedUnknown {
32
+ type: 'unknown';
33
+ }
34
+
35
+ export type DecodedDataBuffer =
36
+ | DecodedTextBuffer
37
+ | DecodedJsonBuffer
38
+ | DecodedAudioBuffer
39
+ | DecodedUnknown;
40
+
41
+ // =============================================================================
42
+ // Encoder Functions
43
+ // =============================================================================
44
+
45
+ /**
46
+ * Encode a text string as a Protobuf DataBuffer with TextBuffer variant
47
+ * @param text - The text string to encode
48
+ * @returns Protobuf-encoded DataBuffer
49
+ */
50
+ export function encodeTextData(text: string): ArrayBuffer;
51
+
52
+ /**
53
+ * Encode a JSON object as a Protobuf DataBuffer with JsonData variant
54
+ * @param data - The object to encode as JSON
55
+ * @param schemaType - Optional schema type identifier
56
+ * @returns Protobuf-encoded DataBuffer
57
+ */
58
+ export function encodeJsonData(data: unknown, schemaType?: string): ArrayBuffer;
59
+
60
+ // =============================================================================
61
+ // Decoder Functions
62
+ // =============================================================================
63
+
64
+ /**
65
+ * Decode a DataBuffer message from binary data
66
+ * @param data - The binary data to decode
67
+ * @returns Decoded DataBuffer
68
+ */
69
+ export function decodeDataBuffer(data: ArrayBuffer | Uint8Array): DecodedDataBuffer;
70
+
71
+ /**
72
+ * Decode a TextBuffer message
73
+ * @param data - The binary data to decode
74
+ * @returns Decoded TextBuffer
75
+ */
76
+ export function decodeTextBuffer(data: Uint8Array): DecodedTextBuffer;
77
+
78
+ /**
79
+ * Decode a JsonData message
80
+ * @param data - The binary data to decode
81
+ * @returns Decoded JsonBuffer
82
+ */
83
+ export function decodeJsonData(data: Uint8Array): DecodedJsonBuffer;
84
+
85
+ /**
86
+ * Decode an AudioBuffer message
87
+ * @param data - The binary data to decode
88
+ * @returns Decoded AudioBuffer
89
+ */
90
+ export function decodeAudioBuffer(data: Uint8Array): DecodedAudioBuffer;
91
+
92
+ /**
93
+ * Parse JSON from a decoded DataBuffer
94
+ * @param decoded - The decoded DataBuffer
95
+ * @returns Parsed JSON object or null
96
+ */
97
+ export function parseJsonFromDataBuffer<T = unknown>(decoded: DecodedDataBuffer): T | null;
package/proto-utils.js ADDED
@@ -0,0 +1,376 @@
1
+ /**
2
+ * Protobuf DataBuffer Encoder/Decoder Utilities
3
+ *
4
+ * Browser and Node.js compatible utilities for encoding/decoding
5
+ * DataBuffer protobuf messages for WebRTC data channel communication.
6
+ *
7
+ * Based on transports/webrtc/protos/common.proto
8
+ */
9
+
10
+ // =============================================================================
11
+ // Encoder Functions
12
+ // =============================================================================
13
+
14
+ /**
15
+ * Calculate the length of a varint encoding
16
+ * @param {number} value
17
+ * @returns {number}
18
+ */
19
+ function varintLength(value) {
20
+ if (value < 0x80) return 1;
21
+ if (value < 0x4000) return 2;
22
+ if (value < 0x200000) return 3;
23
+ if (value < 0x10000000) return 4;
24
+ return 5;
25
+ }
26
+
27
+ /**
28
+ * Write a varint to a buffer
29
+ * @param {Uint8Array} buffer
30
+ * @param {number} offset
31
+ * @param {number} value
32
+ * @returns {number} New offset
33
+ */
34
+ function writeVarint(buffer, offset, value) {
35
+ while (value >= 0x80) {
36
+ buffer[offset++] = (value & 0x7f) | 0x80;
37
+ value >>>= 7;
38
+ }
39
+ buffer[offset++] = value;
40
+ return offset;
41
+ }
42
+
43
+ /**
44
+ * Encode a text string as a Protobuf DataBuffer with TextBuffer variant
45
+ *
46
+ * Wire format:
47
+ * - DataBuffer.text (field 5): tag=0x2a
48
+ * - TextBuffer.text_data (field 1): tag=0x0a
49
+ * - TextBuffer.encoding (field 2): tag=0x12
50
+ *
51
+ * @param {string} text - The text string to encode
52
+ * @returns {ArrayBuffer} Protobuf-encoded DataBuffer
53
+ */
54
+ function encodeTextData(text) {
55
+ const encoder = new TextEncoder();
56
+ const textBytes = encoder.encode(text);
57
+ const encoding = 'utf-8';
58
+ const encodingBytes = encoder.encode(encoding);
59
+
60
+ // Calculate sizes
61
+ const textDataSize = 1 + varintLength(textBytes.length) + textBytes.length;
62
+ const encodingSize = 1 + varintLength(encodingBytes.length) + encodingBytes.length;
63
+ const textBufferSize = textDataSize + encodingSize;
64
+ const dataBufferSize = 1 + varintLength(textBufferSize) + textBufferSize;
65
+
66
+ const buffer = new Uint8Array(dataBufferSize);
67
+ let offset = 0;
68
+
69
+ // DataBuffer.text (field 5, wire type 2)
70
+ buffer[offset++] = (5 << 3) | 2;
71
+ offset = writeVarint(buffer, offset, textBufferSize);
72
+
73
+ // TextBuffer.text_data (field 1, wire type 2)
74
+ buffer[offset++] = (1 << 3) | 2;
75
+ offset = writeVarint(buffer, offset, textBytes.length);
76
+ buffer.set(textBytes, offset);
77
+ offset += textBytes.length;
78
+
79
+ // TextBuffer.encoding (field 2, wire type 2)
80
+ buffer[offset++] = (2 << 3) | 2;
81
+ offset = writeVarint(buffer, offset, encodingBytes.length);
82
+ buffer.set(encodingBytes, offset);
83
+
84
+ return buffer.buffer;
85
+ }
86
+
87
+ /**
88
+ * Encode a JSON object as a Protobuf DataBuffer with JsonData variant
89
+ *
90
+ * Wire format:
91
+ * - DataBuffer.json (field 6): tag=0x32
92
+ * - JsonData.json_payload (field 1): tag=0x0a
93
+ * - JsonData.schema_type (field 2): tag=0x12 [optional]
94
+ *
95
+ * @param {unknown} data - The object to encode as JSON
96
+ * @param {string} [schemaType] - Optional schema type identifier
97
+ * @returns {ArrayBuffer} Protobuf-encoded DataBuffer
98
+ */
99
+ function encodeJsonData(data, schemaType) {
100
+ const encoder = new TextEncoder();
101
+ const jsonString = JSON.stringify(data);
102
+ const jsonBytes = encoder.encode(jsonString);
103
+
104
+ // Calculate JsonData message size
105
+ const jsonPayloadSize = 1 + varintLength(jsonBytes.length) + jsonBytes.length;
106
+ let schemaTypeSize = 0;
107
+ let schemaBytes = null;
108
+
109
+ if (schemaType) {
110
+ schemaBytes = encoder.encode(schemaType);
111
+ schemaTypeSize = 1 + varintLength(schemaBytes.length) + schemaBytes.length;
112
+ }
113
+
114
+ const jsonDataSize = jsonPayloadSize + schemaTypeSize;
115
+ const dataBufferSize = 1 + varintLength(jsonDataSize) + jsonDataSize;
116
+
117
+ const buffer = new Uint8Array(dataBufferSize);
118
+ let offset = 0;
119
+
120
+ // DataBuffer.json (field 6, wire type 2)
121
+ buffer[offset++] = (6 << 3) | 2;
122
+ offset = writeVarint(buffer, offset, jsonDataSize);
123
+
124
+ // JsonData.json_payload (field 1, wire type 2)
125
+ buffer[offset++] = (1 << 3) | 2;
126
+ offset = writeVarint(buffer, offset, jsonBytes.length);
127
+ buffer.set(jsonBytes, offset);
128
+ offset += jsonBytes.length;
129
+
130
+ // JsonData.schema_type (field 2, wire type 2) if provided
131
+ if (schemaBytes) {
132
+ buffer[offset++] = (2 << 3) | 2;
133
+ offset = writeVarint(buffer, offset, schemaBytes.length);
134
+ buffer.set(schemaBytes, offset);
135
+ }
136
+
137
+ return buffer.buffer;
138
+ }
139
+
140
+ // =============================================================================
141
+ // Decoder Functions
142
+ // =============================================================================
143
+
144
+ /**
145
+ * Read a varint from a Uint8Array
146
+ * @param {Uint8Array} buffer
147
+ * @param {number} offset
148
+ * @returns {[number, number]} [value, bytesRead]
149
+ */
150
+ function readVarint(buffer, offset) {
151
+ let result = 0;
152
+ let shift = 0;
153
+ let bytesRead = 0;
154
+
155
+ while (offset + bytesRead < buffer.length) {
156
+ const byte = buffer[offset + bytesRead];
157
+ result |= (byte & 0x7f) << shift;
158
+ bytesRead++;
159
+ if ((byte & 0x80) === 0) {
160
+ break;
161
+ }
162
+ shift += 7;
163
+ }
164
+
165
+ return [result, bytesRead];
166
+ }
167
+
168
+ /**
169
+ * Decode a DataBuffer message from a Uint8Array
170
+ *
171
+ * DataBuffer oneof fields:
172
+ * - field 1: audio (AudioBuffer)
173
+ * - field 2: video (VideoBuffer)
174
+ * - field 3: image (ImageBuffer)
175
+ * - field 4: control (ControlBuffer)
176
+ * - field 5: text (TextBuffer)
177
+ * - field 6: json (JsonData)
178
+ * - field 7: bytes (BytesBuffer)
179
+ *
180
+ * @param {ArrayBuffer|Uint8Array} data
181
+ * @returns {{type: string, [key: string]: unknown}}
182
+ */
183
+ function decodeDataBuffer(data) {
184
+ const buffer = data instanceof Uint8Array ? data : new Uint8Array(data);
185
+ let offset = 0;
186
+
187
+ while (offset < buffer.length) {
188
+ const [tag, tagBytes] = readVarint(buffer, offset);
189
+ offset += tagBytes;
190
+
191
+ const fieldNumber = tag >>> 3;
192
+ const wireType = tag & 0x07;
193
+
194
+ if (wireType === 2) {
195
+ // Length-delimited
196
+ const [length, lengthBytes] = readVarint(buffer, offset);
197
+ offset += lengthBytes;
198
+
199
+ const fieldData = buffer.slice(offset, offset + length);
200
+ offset += length;
201
+
202
+ switch (fieldNumber) {
203
+ case 5: // text (TextBuffer)
204
+ return decodeTextBuffer(fieldData);
205
+ case 6: // json (JsonData)
206
+ return decodeJsonData(fieldData);
207
+ case 1: // audio (AudioBuffer)
208
+ return decodeAudioBuffer(fieldData);
209
+ }
210
+ } else if (wireType === 0) {
211
+ // Varint - skip
212
+ const [, varBytes] = readVarint(buffer, offset);
213
+ offset += varBytes;
214
+ } else if (wireType === 5) {
215
+ // 32-bit fixed
216
+ offset += 4;
217
+ } else if (wireType === 1) {
218
+ // 64-bit fixed
219
+ offset += 8;
220
+ }
221
+ }
222
+
223
+ return { type: 'unknown' };
224
+ }
225
+
226
+ /**
227
+ * Decode a TextBuffer message
228
+ * @param {Uint8Array} data
229
+ * @returns {{type: 'text', textData: string, encoding: string}}
230
+ */
231
+ function decodeTextBuffer(data) {
232
+ const decoder = new TextDecoder();
233
+ let textData = '';
234
+ let encoding = 'utf-8';
235
+ let offset = 0;
236
+
237
+ while (offset < data.length) {
238
+ const [tag, tagBytes] = readVarint(data, offset);
239
+ offset += tagBytes;
240
+
241
+ const fieldNumber = tag >>> 3;
242
+ const wireType = tag & 0x07;
243
+
244
+ if (wireType === 2) {
245
+ const [length, lengthBytes] = readVarint(data, offset);
246
+ offset += lengthBytes;
247
+
248
+ const fieldData = data.slice(offset, offset + length);
249
+ offset += length;
250
+
251
+ if (fieldNumber === 1) {
252
+ textData = decoder.decode(fieldData);
253
+ } else if (fieldNumber === 2) {
254
+ encoding = decoder.decode(fieldData);
255
+ }
256
+ }
257
+ }
258
+
259
+ return { type: 'text', textData, encoding };
260
+ }
261
+
262
+ /**
263
+ * Decode a JsonData message
264
+ * @param {Uint8Array} data
265
+ * @returns {{type: 'json', jsonPayload: string, schemaType?: string}}
266
+ */
267
+ function decodeJsonData(data) {
268
+ const decoder = new TextDecoder();
269
+ let jsonPayload = '';
270
+ let schemaType;
271
+ let offset = 0;
272
+
273
+ while (offset < data.length) {
274
+ const [tag, tagBytes] = readVarint(data, offset);
275
+ offset += tagBytes;
276
+
277
+ const fieldNumber = tag >>> 3;
278
+ const wireType = tag & 0x07;
279
+
280
+ if (wireType === 2) {
281
+ const [length, lengthBytes] = readVarint(data, offset);
282
+ offset += lengthBytes;
283
+
284
+ const fieldData = data.slice(offset, offset + length);
285
+ offset += length;
286
+
287
+ if (fieldNumber === 1) {
288
+ jsonPayload = decoder.decode(fieldData);
289
+ } else if (fieldNumber === 2) {
290
+ schemaType = decoder.decode(fieldData);
291
+ }
292
+ }
293
+ }
294
+
295
+ return { type: 'json', jsonPayload, schemaType };
296
+ }
297
+
298
+ /**
299
+ * Decode an AudioBuffer message
300
+ * @param {Uint8Array} data
301
+ * @returns {{type: 'audio', samples: Float32Array, sampleRate: number, channels: number}}
302
+ */
303
+ function decodeAudioBuffer(data) {
304
+ let samples = new Float32Array(0);
305
+ let sampleRate = 48000;
306
+ let channels = 1;
307
+ let offset = 0;
308
+
309
+ while (offset < data.length) {
310
+ const [tag, tagBytes] = readVarint(data, offset);
311
+ offset += tagBytes;
312
+
313
+ const fieldNumber = tag >>> 3;
314
+ const wireType = tag & 0x07;
315
+
316
+ if (wireType === 2 && fieldNumber === 1) {
317
+ // samples (packed repeated float)
318
+ const [length, lengthBytes] = readVarint(data, offset);
319
+ offset += lengthBytes;
320
+
321
+ const floatCount = length / 4;
322
+ samples = new Float32Array(floatCount);
323
+ const view = new DataView(data.buffer, data.byteOffset + offset, length);
324
+ for (let i = 0; i < floatCount; i++) {
325
+ samples[i] = view.getFloat32(i * 4, true);
326
+ }
327
+ offset += length;
328
+ } else if (wireType === 0) {
329
+ const [value, varBytes] = readVarint(data, offset);
330
+ offset += varBytes;
331
+
332
+ if (fieldNumber === 2) {
333
+ sampleRate = value;
334
+ } else if (fieldNumber === 3) {
335
+ channels = value;
336
+ }
337
+ } else if (wireType === 5) {
338
+ offset += 4;
339
+ }
340
+ }
341
+
342
+ return { type: 'audio', samples, sampleRate, channels };
343
+ }
344
+
345
+ /**
346
+ * Parse JSON from a decoded DataBuffer
347
+ * @template T
348
+ * @param {{type: string, jsonPayload?: string}} decoded
349
+ * @returns {T|null}
350
+ */
351
+ function parseJsonFromDataBuffer(decoded) {
352
+ if (decoded.type === 'json' && decoded.jsonPayload) {
353
+ try {
354
+ return JSON.parse(decoded.jsonPayload);
355
+ } catch {
356
+ return null;
357
+ }
358
+ }
359
+ return null;
360
+ }
361
+
362
+ // =============================================================================
363
+ // Exports
364
+ // =============================================================================
365
+
366
+ module.exports = {
367
+ // Encoders
368
+ encodeTextData,
369
+ encodeJsonData,
370
+ // Decoders
371
+ decodeDataBuffer,
372
+ decodeTextBuffer,
373
+ decodeJsonData,
374
+ decodeAudioBuffer,
375
+ parseJsonFromDataBuffer,
376
+ };