@kelnishi/satmouse-client 0.14.0 → 0.15.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.
@@ -252,6 +252,114 @@ var WebSocketAdapter = class {
252
252
  }
253
253
  };
254
254
 
255
+ // src/core/transports/webrtc.ts
256
+ var WebRTCAdapter = class {
257
+ protocol = "webrtc";
258
+ onSpatialData = null;
259
+ onButtonEvent = null;
260
+ onDeviceStatus = null;
261
+ onClose = null;
262
+ onError = null;
263
+ pc = null;
264
+ signalingUrl;
265
+ /**
266
+ * @param signalingUrl — HTTP endpoint for SDP exchange (e.g., "http://127.0.0.1:18945/rtc/offer")
267
+ */
268
+ constructor(signalingUrl) {
269
+ this.signalingUrl = signalingUrl;
270
+ }
271
+ async connect() {
272
+ if (typeof globalThis.RTCPeerConnection === "undefined") {
273
+ throw new Error("RTCPeerConnection not available");
274
+ }
275
+ this.pc = new RTCPeerConnection({
276
+ iceServers: []
277
+ // Local network, no STUN/TURN
278
+ });
279
+ this.pc.ondatachannel = (event) => {
280
+ const channel = event.channel;
281
+ if (channel.label === "spatial") {
282
+ channel.binaryType = "arraybuffer";
283
+ channel.onmessage = (e) => {
284
+ if (e.data instanceof ArrayBuffer && e.data.byteLength >= 20) {
285
+ const decoded = decodeBinaryFrame(e.data);
286
+ this.onSpatialData?.(decoded);
287
+ }
288
+ };
289
+ } else if (channel.label === "reliable") {
290
+ channel.onmessage = (e) => {
291
+ try {
292
+ const text = typeof e.data === "string" ? e.data : new TextDecoder().decode(e.data);
293
+ const msg = JSON.parse(text);
294
+ if (msg.type === "buttonEvent") this.onButtonEvent?.(msg.data);
295
+ else if (msg.type === "deviceStatus") this.onDeviceStatus?.(msg.data.event, msg.data.device);
296
+ } catch {
297
+ }
298
+ };
299
+ }
300
+ };
301
+ this.pc.onconnectionstatechange = () => {
302
+ const state = this.pc?.connectionState;
303
+ if (state === "disconnected" || state === "failed" || state === "closed") {
304
+ this.onClose?.();
305
+ }
306
+ };
307
+ const offer = await this.pc.createOffer();
308
+ await this.pc.setLocalDescription(offer);
309
+ await this.waitForICE();
310
+ const response = await fetch(this.signalingUrl, {
311
+ method: "POST",
312
+ body: this.pc.localDescription.sdp,
313
+ headers: { "Content-Type": "application/sdp" }
314
+ });
315
+ if (!response.ok) {
316
+ throw new Error(`Signaling failed: ${response.status}`);
317
+ }
318
+ const answerSdp = await response.text();
319
+ await this.pc.setRemoteDescription({ type: "answer", sdp: answerSdp });
320
+ await new Promise((resolve, reject) => {
321
+ const timeout = setTimeout(() => reject(new Error("WebRTC connection timeout")), 1e4);
322
+ const check = () => {
323
+ if (this.pc?.connectionState === "connected") {
324
+ clearTimeout(timeout);
325
+ resolve();
326
+ } else if (this.pc?.connectionState === "failed") {
327
+ clearTimeout(timeout);
328
+ reject(new Error("WebRTC connection failed"));
329
+ }
330
+ };
331
+ this.pc.onconnectionstatechange = () => {
332
+ check();
333
+ const state = this.pc?.connectionState;
334
+ if (state === "disconnected" || state === "failed" || state === "closed") {
335
+ this.onClose?.();
336
+ }
337
+ };
338
+ check();
339
+ });
340
+ }
341
+ close() {
342
+ try {
343
+ this.pc?.close();
344
+ } catch {
345
+ }
346
+ this.pc = null;
347
+ }
348
+ waitForICE() {
349
+ return new Promise((resolve) => {
350
+ if (!this.pc) return resolve();
351
+ if (this.pc.iceGatheringState === "complete") return resolve();
352
+ const timeout = setTimeout(resolve, 2e3);
353
+ this.pc.onicegatheringstatechange = () => {
354
+ if (this.pc?.iceGatheringState === "complete") {
355
+ clearTimeout(timeout);
356
+ resolve();
357
+ }
358
+ };
359
+ });
360
+ }
361
+ };
362
+
255
363
  // src/core/connection.ts
256
364
  function parseSatMouseUri(uri) {
257
365
  const url = new URL(uri);
@@ -265,7 +373,7 @@ function parseSatMouseUri(uri) {
265
373
  };
266
374
  }
267
375
  var DEFAULT_OPTIONS = {
268
- transports: ["webtransport", "websocket"],
376
+ transports: ["webtransport", "webrtc", "websocket"],
269
377
  reconnectDelay: 2e3,
270
378
  maxRetries: 3,
271
379
  wsSubprotocol: "satmouse-json"
@@ -323,7 +431,8 @@ var SatMouseConnection = class extends TypedEmitter {
323
431
  }
324
432
  if (!resolved) {
325
433
  this.emit("error", new Error("Failed to fetch Thing Description"));
326
- wsUrl = "ws://127.0.0.1:18945/spatial";
434
+ const isSecurePage = typeof globalThis.location !== "undefined" && globalThis.location.protocol === "https:";
435
+ wsUrl = isSecurePage ? "wss://127.0.0.1:18947/spatial" : "ws://127.0.0.1:18945/spatial";
327
436
  }
328
437
  }
329
438
  for (const proto of this.options.transports) {
@@ -336,6 +445,16 @@ var SatMouseConnection = class extends TypedEmitter {
336
445
  continue;
337
446
  }
338
447
  }
448
+ if (proto === "webrtc") {
449
+ try {
450
+ if (typeof globalThis.RTCPeerConnection === "undefined") continue;
451
+ const rtcUrl = this.options.rtcUrl ?? `http://127.0.0.1:18945/rtc/offer`;
452
+ const adapter = new WebRTCAdapter(rtcUrl);
453
+ if (await this.tryTransport(adapter)) return;
454
+ } catch {
455
+ continue;
456
+ }
457
+ }
339
458
  if (proto === "websocket" && wsUrl) {
340
459
  try {
341
460
  const adapter = new WebSocketAdapter(wsUrl, this.options.wsSubprotocol);