@decentnetwork/lan 0.1.175 → 0.1.176

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.
Binary file
Binary file
Binary file
Binary file
@@ -20,6 +20,7 @@ const ICON_PATHS = {
20
20
  qr: '<rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/><path d="M14 14h3v3M21 14v.01M14 21h.01M17 21h.01M21 17v4"/>',
21
21
  // ---- app tiles ----
22
22
  video: '<path d="m22 8.5-5.4 3.5 5.4 3.5V8.5Z"/><rect x="2" y="6" width="14.5" height="12" rx="3"/>',
23
+ monitor: '<rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/>',
23
24
  sparkles: '<path d="M12 3.5 13.6 9 19 10.5 13.6 12 12 17.5 10.4 12 5 10.5 10.4 9 12 3.5Z"/><path d="M19 3.5v3.2M20.6 5.1h-3.2"/><path d="M5 16v2.4M6.2 17.2H3.8"/>',
24
25
  mic: '<rect x="9" y="2.5" width="6" height="11.5" rx="3"/><path d="M5.5 11a6.5 6.5 0 0 0 13 0"/><path d="M12 17.5v3.5"/>',
25
26
  flask: '<path d="M9.5 3v6.2L4.8 17.4A2 2 0 0 0 6.5 20.5h11a2 2 0 0 0 1.7-3.1L14.5 9.2V3"/><path d="M8.5 3h7"/><path d="M7.3 15h9.4"/>',
@@ -1596,6 +1597,7 @@ function useCallController(selfId) {
1596
1597
  const [active, setActive] = React.useState(null);
1597
1598
  const [localStream, setLocalStream] = React.useState(null);
1598
1599
  const [remoteStream, setRemoteStream] = React.useState(null);
1600
+ const [sharing, setSharing] = React.useState(false);
1599
1601
  const engineRef = React.useRef(null);
1600
1602
  React.useEffect(() => {
1601
1603
  if (!selfId || engineRef.current) return;
@@ -1610,12 +1612,19 @@ function useCallController(selfId) {
1610
1612
  signaling,
1611
1613
  createPeerConnection: (c) => new RTCPeerConnection(c),
1612
1614
  getLocalMedia: (k) => navigator.mediaDevices.getUserMedia({ audio: k.audio, video: k.video }),
1615
+ getDisplayMedia: (c) => navigator.mediaDevices.getDisplayMedia(c || { video: true, audio: false }),
1613
1616
  iceServers: CALL_ICE_SERVERS,
1614
1617
  logger: (m) => console.log(m)
1615
1618
  });
1616
1619
  engine.on("incomingCall", (info) => setIncoming(info));
1617
1620
  engine.on("stateChanged", (info, state) => setActive((a) => a && a.callId === info.callId ? { ...a, state } : a));
1618
- engine.on("localStream", (id, s) => setLocalStream(s));
1621
+ engine.on("localStream", (id, s) => {
1622
+ setLocalStream(s);
1623
+ try {
1624
+ setSharing(engine.isSharingScreen(id));
1625
+ } catch (e) {
1626
+ }
1627
+ });
1619
1628
  engine.on("remoteStream", (id, s) => setRemoteStream(s));
1620
1629
  engine.on("ended", (id) => {
1621
1630
  setActive((a) => a && a.callId === id ? null : a);
@@ -1675,11 +1684,21 @@ function useCallController(selfId) {
1675
1684
  const eng = engineRef.current, a = active;
1676
1685
  if (eng && a && a.callId) eng.setLocalTrackEnabled(a.callId, "audio", !m);
1677
1686
  }, [active]);
1687
+ const toggleShare = React.useCallback(async () => {
1688
+ const eng = engineRef.current, a = active;
1689
+ if (!eng || !a || !a.callId) return;
1690
+ try {
1691
+ if (eng.isSharingScreen(a.callId)) await eng.stopScreenShare(a.callId);
1692
+ else await eng.shareScreen(a.callId);
1693
+ } catch (e) {
1694
+ if (e && e.name !== "NotAllowedError") alert("Screen share failed: " + (e && e.message || e));
1695
+ }
1696
+ }, [active]);
1678
1697
  const setVideoOn = React.useCallback((v) => {
1679
1698
  const eng = engineRef.current, a = active;
1680
1699
  if (eng && a && a.callId) eng.setLocalTrackEnabled(a.callId, "video", v);
1681
1700
  }, [active]);
1682
- return { incoming, active, localStream, remoteStream, start, accept, reject, hangup, setMuted, setVideoOn };
1701
+ return { incoming, active, localStream, remoteStream, sharing, start, accept, reject, hangup, setMuted, setVideoOn, toggleShare };
1683
1702
  }
1684
1703
  function VideoSurface({ stream, muted, style }) {
1685
1704
  const ref = React.useCallback((el) => {
@@ -1789,7 +1808,8 @@ function CallOverlay({ T, ctl, peers }) {
1789
1808
  setMuted(n);
1790
1809
  ctl.setMuted(n);
1791
1810
  };
1792
- const controls = /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: 18, alignItems: "flex-start" } }, /* @__PURE__ */ React.createElement(CallBtn, { icon: muted ? "micFill" : "mic", label: muted ? T && T.unmute || "unmute" : T && T.mute || "mute", active: muted, onClick: toggleMute }), /* @__PURE__ */ React.createElement(CallBtn, { icon: "phone", label: T && T.endCall || "end", danger: true, wide: true, onClick: ctl.hangup }));
1811
+ const canShare = connected && typeof navigator !== "undefined" && navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia;
1812
+ const controls = /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: 18, alignItems: "flex-start" } }, /* @__PURE__ */ React.createElement(CallBtn, { icon: muted ? "micFill" : "mic", label: muted ? T && T.unmute || "unmute" : T && T.mute || "mute", active: muted, onClick: toggleMute }), canShare && /* @__PURE__ */ React.createElement(CallBtn, { icon: "monitor", label: ctl.sharing ? T && T.stopShare || "stop share" : T && T.share || "share", active: ctl.sharing, onClick: ctl.toggleShare }), /* @__PURE__ */ React.createElement(CallBtn, { icon: "phone", label: T && T.endCall || "end", danger: true, wide: true, onClick: ctl.hangup }));
1793
1813
  return /* @__PURE__ */ React.createElement("div", { style: { position: "fixed", inset: 0, zIndex: 70, background: "var(--bg)", display: "flex", flexDirection: "column" } }, ctl.remoteStream && /* @__PURE__ */ React.createElement(AudioSink, { stream: ctl.remoteStream }), /* @__PURE__ */ React.createElement("div", { style: { height: 52, flexShrink: 0, display: "flex", alignItems: "center", gap: 10, padding: "0 18px", borderBottom: "1px solid var(--line)" } }, /* @__PURE__ */ React.createElement(Icon, { name: video ? "video" : "phone", size: 16, color: "var(--accent)", stroke: 2 }), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 13, fontWeight: 700, color: "var(--text)" } }, name), /* @__PURE__ */ React.createElement(Tag, { tone: connected ? "ok" : "warn" }, stateLabel), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), connected && /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 14, fontWeight: 700, color: "var(--text)", letterSpacing: 1 } }, mm, ":", ss)), video ? /* @__PURE__ */ React.createElement("div", { style: { flex: 1, position: "relative", padding: 18 } }, /* @__PURE__ */ React.createElement("div", { style: { position: "absolute", inset: 18, borderRadius: 14, overflow: "hidden", background: "#000", border: "1px solid var(--line)" } }, ctl.remoteStream ? /* @__PURE__ */ React.createElement(VideoSurface, { stream: ctl.remoteStream, muted: true, style: { width: "100%", height: "100%", objectFit: "cover" } }) : /* @__PURE__ */ React.createElement("div", { style: { width: "100%", height: "100%", display: "flex", alignItems: "center", justifyContent: "center" } }, /* @__PURE__ */ React.createElement(DkIdenticon, { seed: call.peerId, size: 92, radius: 22 }))), /* @__PURE__ */ React.createElement("div", { style: { position: "absolute", right: 30, bottom: 30, width: 220, height: 148, borderRadius: 12, overflow: "hidden", background: "#000", border: "1px solid var(--line)" } }, ctl.localStream ? /* @__PURE__ */ React.createElement(VideoSurface, { stream: ctl.localStream, muted: true, style: { width: "100%", height: "100%", objectFit: "cover", transform: "scaleX(-1)" } }) : /* @__PURE__ */ React.createElement("div", { style: { width: "100%", height: "100%", display: "flex", alignItems: "center", justifyContent: "center" } }, /* @__PURE__ */ React.createElement(Icon, { name: "userRound", size: 30, color: "var(--faint)", stroke: 1.4 }))), /* @__PURE__ */ React.createElement("div", { style: { position: "absolute", left: 0, right: 0, bottom: 30, display: "flex", justifyContent: "center" } }, /* @__PURE__ */ React.createElement("div", { style: { padding: "16px 26px", borderRadius: 20, background: "color-mix(in oklab, var(--panel), transparent 8%)", border: "1px solid var(--line)" } }, controls))) : /* @__PURE__ */ React.createElement("div", { style: { flex: 1, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 22 } }, /* @__PURE__ */ React.createElement("div", { style: { position: "relative", display: "flex", alignItems: "center", justifyContent: "center" } }, !connected && /* @__PURE__ */ React.createElement("span", { style: { position: "absolute", width: 150, height: 150, borderRadius: 999, border: "2px solid var(--accent)", animation: "dkpulse 2s ease-out infinite" } }), /* @__PURE__ */ React.createElement(DkIdenticon, { seed: call.peerId, size: 108, radius: 26 })), /* @__PURE__ */ React.createElement("div", { style: { textAlign: "center" } }, /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 22, fontWeight: 700, color: "var(--text)" } }, name), /* @__PURE__ */ React.createElement("div", { style: { marginTop: 8, fontFamily: "var(--mono)", fontSize: 12.5, color: "var(--faint)" } }, stateLabel)), /* @__PURE__ */ React.createElement("div", { style: { marginTop: 18 } }, controls)));
1794
1814
  }
1795
1815
  Object.assign(window, { useCallController, CallOverlay, IncomingCallModal });
@@ -153,7 +153,7 @@ var PeerWebRTC = (() => {
153
153
  var DEFAULT_ICE_SERVERS = [
154
154
  { urls: "stun:stun.l.google.com:19302" }
155
155
  ];
156
- var _opts, _iceServers, _sessions, _CallEngine_instances, handleSignal_fn, onOffer_fn, onAnswer_fn, onCandidate_fn, onBye_fn, onAuxiliary_fn, createSession_fn, wirePeerConnection_fn, ensureRemoteStream_fn, attachLocalMedia_fn, flushLocalCandidates_fn, drainRemoteCandidates_fn, send_fn, sendByeSafe_fn, cleanup_fn, setState_fn, normId_fn, _toInfo, newCallId_fn, log_fn;
156
+ var _opts, _iceServers, _sessions, _CallEngine_instances, setOutgoingVideoTrack_fn, renegotiate_fn, handleSignal_fn, onOffer_fn, answerRenegotiation_fn, onAnswer_fn, onCandidate_fn, onBye_fn, onAuxiliary_fn, createSession_fn, wirePeerConnection_fn, ensureRemoteStream_fn, attachLocalMedia_fn, flushLocalCandidates_fn, drainRemoteCandidates_fn, send_fn, sendByeSafe_fn, cleanup_fn, setState_fn, normId_fn, _toInfo, newCallId_fn, log_fn;
157
157
  var CallEngine = class extends Emitter {
158
158
  constructor(opts) {
159
159
  var _a;
@@ -286,6 +286,50 @@ var PeerWebRTC = (() => {
286
286
  track.enabled = enabled;
287
287
  }
288
288
  }
289
+ /** Start sharing the screen on an active call. Captures the display, sends it
290
+ * as the outgoing video track (replacing the camera if any), and renegotiates
291
+ * so the peer receives it. Works even with no camera. Ending the OS "stop
292
+ * sharing" prompt reverts automatically. */
293
+ async shareScreen(callId) {
294
+ const session = __privateGet(this, _sessions).get(__privateMethod(this, _CallEngine_instances, normId_fn).call(this, callId));
295
+ if (!session)
296
+ throw new Error(`shareScreen: no call ${callId}`);
297
+ if (!__privateGet(this, _opts).getDisplayMedia)
298
+ throw new Error("screen share not supported here");
299
+ const screen = await __privateGet(this, _opts).getDisplayMedia({ video: true, audio: false });
300
+ const track = screen.getVideoTracks()[0];
301
+ if (!track)
302
+ return;
303
+ if (!session.screenStream)
304
+ session.cameraStream = session.localStream;
305
+ session.screenStream = screen;
306
+ await __privateMethod(this, _CallEngine_instances, setOutgoingVideoTrack_fn).call(this, session, track, screen);
307
+ track.addEventListener("ended", () => {
308
+ void this.stopScreenShare(callId);
309
+ });
310
+ await __privateMethod(this, _CallEngine_instances, renegotiate_fn).call(this, session);
311
+ __privateMethod(this, _CallEngine_instances, log_fn).call(this, `shareScreen started for ${callId}`);
312
+ }
313
+ /** Stop screen sharing; restore the camera track if there was one. */
314
+ async stopScreenShare(callId) {
315
+ var _a;
316
+ const session = __privateGet(this, _sessions).get(__privateMethod(this, _CallEngine_instances, normId_fn).call(this, callId));
317
+ if (!(session == null ? void 0 : session.screenStream))
318
+ return;
319
+ for (const t of session.screenStream.getTracks())
320
+ t.stop();
321
+ session.screenStream = void 0;
322
+ const camTrack = (_a = session.cameraStream) == null ? void 0 : _a.getVideoTracks()[0];
323
+ await __privateMethod(this, _CallEngine_instances, setOutgoingVideoTrack_fn).call(this, session, camTrack != null ? camTrack : null, session.cameraStream);
324
+ session.cameraStream = void 0;
325
+ await __privateMethod(this, _CallEngine_instances, renegotiate_fn).call(this, session);
326
+ __privateMethod(this, _CallEngine_instances, log_fn).call(this, `shareScreen stopped for ${callId}`);
327
+ }
328
+ /** True when the call is currently sharing the screen. */
329
+ isSharingScreen(callId) {
330
+ var _a;
331
+ return !!((_a = __privateGet(this, _sessions).get(__privateMethod(this, _CallEngine_instances, normId_fn).call(this, callId))) == null ? void 0 : _a.screenStream);
332
+ }
289
333
  /** Tear down every call (e.g. on app shutdown). */
290
334
  dispose() {
291
335
  for (const session of [...__privateGet(this, _sessions).values()]) {
@@ -298,6 +342,44 @@ var PeerWebRTC = (() => {
298
342
  _iceServers = new WeakMap();
299
343
  _sessions = new WeakMap();
300
344
  _CallEngine_instances = new WeakSet();
345
+ setOutgoingVideoTrack_fn = async function(session, track, stream) {
346
+ var _a;
347
+ const transceivers = session.pc.getTransceivers();
348
+ const tr = (_a = transceivers.find((t) => {
349
+ var _a2;
350
+ return ((_a2 = t.sender.track) == null ? void 0 : _a2.kind) === "video";
351
+ })) != null ? _a : transceivers.find((t) => {
352
+ var _a2;
353
+ return ((_a2 = t.receiver.track) == null ? void 0 : _a2.kind) === "video";
354
+ });
355
+ if (tr) {
356
+ await tr.sender.replaceTrack(track);
357
+ if (track) {
358
+ if (tr.direction === "recvonly")
359
+ tr.direction = "sendrecv";
360
+ else if (tr.direction === "inactive")
361
+ tr.direction = "sendonly";
362
+ }
363
+ } else if (track && stream) {
364
+ session.pc.addTrack(track, stream);
365
+ }
366
+ session.localStream = stream;
367
+ this.emit("localStream", session.callId, stream != null ? stream : new MediaStream());
368
+ };
369
+ renegotiate_fn = async function(session) {
370
+ try {
371
+ const offer = await session.pc.createOffer();
372
+ await session.pc.setLocalDescription(offer);
373
+ const options = [];
374
+ if (session.audio)
375
+ options.push("audio");
376
+ if (session.video || session.screenStream)
377
+ options.push("video");
378
+ await __privateMethod(this, _CallEngine_instances, send_fn).call(this, session.peerId, { type: "offer", sdp: offer.sdp, options, callId: session.callId });
379
+ } catch (err) {
380
+ __privateMethod(this, _CallEngine_instances, log_fn).call(this, `renegotiate failed: ${err.message}`);
381
+ }
382
+ };
301
383
  // ---- internals -------------------------------------------------------
302
384
  handleSignal_fn = function(peerId, signal) {
303
385
  switch (signal.type) {
@@ -328,7 +410,10 @@ var PeerWebRTC = (() => {
328
410
  return;
329
411
  const existing = __privateGet(this, _sessions).get(__privateMethod(this, _CallEngine_instances, normId_fn).call(this, signal.callId));
330
412
  if (existing) {
331
- existing.pendingOffer = signal.sdp;
413
+ if (existing.hasReceivedSdp)
414
+ void __privateMethod(this, _CallEngine_instances, answerRenegotiation_fn).call(this, existing, signal.sdp);
415
+ else
416
+ existing.pendingOffer = signal.sdp;
332
417
  return;
333
418
  }
334
419
  const audio = signal.options ? signal.options.includes("audio") : true;
@@ -339,6 +424,16 @@ var PeerWebRTC = (() => {
339
424
  this.emit("incomingCall", __privateGet(this, _toInfo).call(this, session));
340
425
  __privateMethod(this, _CallEngine_instances, log_fn).call(this, `incoming call from ${peerId} callId=${signal.callId} audio=${audio} video=${video}`);
341
426
  };
427
+ answerRenegotiation_fn = async function(session, sdp) {
428
+ try {
429
+ await session.pc.setRemoteDescription({ type: "offer", sdp });
430
+ const answer = await session.pc.createAnswer();
431
+ await session.pc.setLocalDescription(answer);
432
+ await __privateMethod(this, _CallEngine_instances, send_fn).call(this, session.peerId, { type: "answer", sdp: answer.sdp, callId: session.callId });
433
+ } catch (err) {
434
+ __privateMethod(this, _CallEngine_instances, log_fn).call(this, `renegotiation answer failed: ${err.message}`);
435
+ }
436
+ };
342
437
  onAnswer_fn = function(peerId, signal) {
343
438
  const session = __privateGet(this, _sessions).get(__privateMethod(this, _CallEngine_instances, normId_fn).call(this, signal.callId));
344
439
  if (!session || !signal.sdp)
@@ -348,7 +443,8 @@ var PeerWebRTC = (() => {
348
443
  try {
349
444
  await session.pc.setRemoteDescription({ type: "answer", sdp: signal.sdp });
350
445
  session.hasReceivedSdp = true;
351
- __privateMethod(this, _CallEngine_instances, setState_fn).call(this, session, "connecting");
446
+ if (session.state !== "connected")
447
+ __privateMethod(this, _CallEngine_instances, setState_fn).call(this, session, "connecting");
352
448
  await __privateMethod(this, _CallEngine_instances, drainRemoteCandidates_fn).call(this, session);
353
449
  await __privateMethod(this, _CallEngine_instances, flushLocalCandidates_fn).call(this, session);
354
450
  } catch (err) {
@@ -439,12 +535,8 @@ var PeerWebRTC = (() => {
439
535
  }
440
536
  };
441
537
  pc.ontrack = (ev) => {
442
- var _a;
443
- const stream = (_a = ev.streams[0]) != null ? _a : __privateMethod(this, _CallEngine_instances, ensureRemoteStream_fn).call(this, session, ev.track);
444
- if (session.remoteStream !== stream) {
445
- session.remoteStream = stream;
446
- this.emit("remoteStream", session.callId, stream);
447
- }
538
+ const stream = __privateMethod(this, _CallEngine_instances, ensureRemoteStream_fn).call(this, session, ev.track);
539
+ this.emit("remoteStream", session.callId, stream);
448
540
  };
449
541
  pc.oniceconnectionstatechange = () => {
450
542
  switch (pc.iceConnectionState) {
@@ -471,7 +563,8 @@ var PeerWebRTC = (() => {
471
563
  ensureRemoteStream_fn = function(session, track) {
472
564
  if (!session.remoteStream)
473
565
  session.remoteStream = new MediaStream();
474
- session.remoteStream.addTrack(track);
566
+ if (!session.remoteStream.getTracks().includes(track))
567
+ session.remoteStream.addTrack(track);
475
568
  return session.remoteStream;
476
569
  };
477
570
  attachLocalMedia_fn = async function(session) {
package/dist/ui/server.js CHANGED
@@ -198,11 +198,13 @@ export function startFriendUi(opts) {
198
198
  const file = join(DESKTOP_DIR, rel);
199
199
  // Guard against path traversal: resolved file must stay under DESKTOP_DIR.
200
200
  if (existsSync(file) && file.startsWith(DESKTOP_DIR)) {
201
- // app.js changes on every UI rebuild never cache it. Vendored React
202
- // UMD is stable, so let it cache normally.
201
+ // app.js AND vendor/peer-webrtc.js change on every rebuild / SDK bump
202
+ // never cache them, or the browser keeps running stale call/UI code
203
+ // (this silently defeated every peer-webrtc fix). Only the truly stable
204
+ // vendored libs (React UMD) may cache.
203
205
  const headers = { "content-type": "application/javascript; charset=utf-8" };
204
- if (rel === "app.js")
205
- headers["cache-control"] = "no-store";
206
+ const volatile = rel === "app.js" || rel.includes("peer-webrtc");
207
+ headers["cache-control"] = volatile ? "no-store" : "public, max-age=31536000";
206
208
  res.writeHead(200, headers);
207
209
  res.end(readFileSync(file));
208
210
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/lan",
3
- "version": "0.1.175",
3
+ "version": "0.1.176",
4
4
  "description": "Private virtual LAN for self-hosted services and AI agents, built on Elastos Carrier. NAT-traversal, name service, ACL, all over a peer-to-peer mesh — no public IP required.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -80,7 +80,7 @@
80
80
  "dependencies": {
81
81
  "@decentnetwork/dora": "^0.1.13",
82
82
  "@decentnetwork/peer": "^0.1.91",
83
- "@decentnetwork/peer-webrtc": "^0.2.7",
83
+ "@decentnetwork/peer-webrtc": "^0.2.8",
84
84
  "ink": "^5.2.1",
85
85
  "js-yaml": "^4.1.0",
86
86
  "node-forge": "^1.4.0",