@decentnetwork/lan 0.1.254 → 0.1.256
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.
|
@@ -27,6 +27,17 @@ export interface PeerManagerOptions {
|
|
|
27
27
|
* signals over socket.io instead of failing. */
|
|
28
28
|
offlineCall?: OfflineCallConfig;
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* True when a Carrier invite send failed for a reason that will recur
|
|
32
|
+
* identically on every retry, so retrying or waiting for a reconnect is
|
|
33
|
+
* pointless and the signal must take another transport or be lost.
|
|
34
|
+
*
|
|
35
|
+
* The one that bit us: the native invite channel caps application data at
|
|
36
|
+
* CARRIER_MAX_INVITE_DATA_LEN (8192, from carrier.h — it is a wire constant
|
|
37
|
+
* shared with the iOS/Android SDK, not something we can raise unilaterally),
|
|
38
|
+
* while a Chrome SDP offer/answer runs 8.4-9.8 KB.
|
|
39
|
+
*/
|
|
40
|
+
export declare function isPermanentInviteFailure(err: unknown): boolean;
|
|
30
41
|
export declare class PeerManager extends EventEmitter {
|
|
31
42
|
private peer;
|
|
32
43
|
private identity;
|
|
@@ -9,6 +9,29 @@ import { PacketSession } from "./packet-session.js";
|
|
|
9
9
|
import { FrameCodec } from "./frame.js";
|
|
10
10
|
import { FRAME_OPCODE_HANDSHAKE_ACK, PACKET_ID_DL_SESSION, PACKET_ID_DL_DORA, PACKET_ID_DL_IP, PACKET_ID_DL_RATE, } from "./types.js";
|
|
11
11
|
import { Logger } from "../utils/logger.js";
|
|
12
|
+
/**
|
|
13
|
+
* True when a Carrier invite send failed for a reason that will recur
|
|
14
|
+
* identically on every retry, so retrying or waiting for a reconnect is
|
|
15
|
+
* pointless and the signal must take another transport or be lost.
|
|
16
|
+
*
|
|
17
|
+
* The one that bit us: the native invite channel caps application data at
|
|
18
|
+
* CARRIER_MAX_INVITE_DATA_LEN (8192, from carrier.h — it is a wire constant
|
|
19
|
+
* shared with the iOS/Android SDK, not something we can raise unilaterally),
|
|
20
|
+
* while a Chrome SDP offer/answer runs 8.4-9.8 KB.
|
|
21
|
+
*/
|
|
22
|
+
export function isPermanentInviteFailure(err) {
|
|
23
|
+
return /invite data must be/i.test(err?.message ?? "");
|
|
24
|
+
}
|
|
25
|
+
/** Best-effort RtcSignal type ("offer"/"answer"/"candidate"/"bye") for logs. */
|
|
26
|
+
function signalKind(data) {
|
|
27
|
+
try {
|
|
28
|
+
const text = typeof data === "string" ? data : Buffer.from(data).toString("utf-8");
|
|
29
|
+
return String(JSON.parse(text).type ?? "signal");
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return "signal";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
12
35
|
export class PeerManager extends EventEmitter {
|
|
13
36
|
peer = null;
|
|
14
37
|
identity = null;
|
|
@@ -181,7 +204,21 @@ export class PeerManager extends EventEmitter {
|
|
|
181
204
|
// A call already committed to Carrier must NOT split to socket.io on a
|
|
182
205
|
// transient flap (it breaks ICE). Drop this signal; ICE recovers via the
|
|
183
206
|
// other candidates / a retried offer.
|
|
207
|
+
//
|
|
208
|
+
// That reasoning only holds for a TRANSIENT failure. An oversize payload
|
|
209
|
+
// fails identically on every retry, so "ICE recovers via a retried offer"
|
|
210
|
+
// is false — the retry is the same too-big offer. Chrome's SDP runs
|
|
211
|
+
// 8.4-9.8 KB against the 8192-byte native invite cap (carrier.h), which
|
|
212
|
+
// silently killed every desktop→phone call: the offer never left, so the
|
|
213
|
+
// phone never rang, and on an inbound call our answer never left, so it
|
|
214
|
+
// rang and then never connected. Route a permanently-undeliverable signal
|
|
215
|
+
// to the offline bridge instead of dropping it on the floor.
|
|
184
216
|
if (callId && this.offlineBridge?.isOnlineCall(callId)) {
|
|
217
|
+
if (this.offlineBridge.enabled && isPermanentInviteFailure(err)) {
|
|
218
|
+
this.logger.warn(`sendCallSignal: ${signalKind(data)} for call ${callId} cannot go over Carrier (${err.message}) — offline path to ${userid}`);
|
|
219
|
+
await this.offlineBridge.sendSignal(userid, data, hasVideo);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
185
222
|
this.logger.info(`sendCallSignal: dropped a Carrier signal for online call ${callId} (${err.message})`);
|
|
186
223
|
return;
|
|
187
224
|
}
|
package/dist/ui/desktop/app.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
window.__DK_UI_VERSION="0.1.
|
|
1
|
+
window.__DK_UI_VERSION="0.1.256";
|
|
2
2
|
const ICON_PATHS = {
|
|
3
3
|
// ---- tab bar (the four must feel like one set) ----
|
|
4
4
|
users: '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>',
|
|
@@ -2454,6 +2454,40 @@ const CALL_ICE_SERVERS = [
|
|
|
2454
2454
|
{ urls: "turn:tokyo.fi.chat:3478", username: "allcom", credential: "allcompass" },
|
|
2455
2455
|
{ urls: "turn:tokyo.fi.chat:3478?transport=tcp", username: "allcom", credential: "allcompass" }
|
|
2456
2456
|
];
|
|
2457
|
+
const VIDEO_CODEC_KEEP = /^video\/(H264|VP8|rtx|red|ulpfec)$/i;
|
|
2458
|
+
const SDP_BUDGET = 8192;
|
|
2459
|
+
function trimVideoCodecs(pc) {
|
|
2460
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
2461
|
+
const supported = (_c = (_b = (_a = window.RTCRtpSender) == null ? void 0 : _a.getCapabilities) == null ? void 0 : _b.call(_a, "video")) == null ? void 0 : _c.codecs;
|
|
2462
|
+
if (!supported) return;
|
|
2463
|
+
const keep = supported.filter((c) => VIDEO_CODEC_KEEP.test(c.mimeType));
|
|
2464
|
+
if (!keep.length || keep.length === supported.length) return;
|
|
2465
|
+
for (const t of pc.getTransceivers()) {
|
|
2466
|
+
const kind = ((_e = (_d = t.receiver) == null ? void 0 : _d.track) == null ? void 0 : _e.kind) || ((_g = (_f = t.sender) == null ? void 0 : _f.track) == null ? void 0 : _g.kind);
|
|
2467
|
+
if (kind !== "video" || !t.setCodecPreferences) continue;
|
|
2468
|
+
try {
|
|
2469
|
+
t.setCodecPreferences(keep);
|
|
2470
|
+
} catch (e) {
|
|
2471
|
+
console.warn("[call] setCodecPreferences failed", e);
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
function createLeanPeerConnection(config) {
|
|
2476
|
+
const pc = new RTCPeerConnection(config);
|
|
2477
|
+
for (const method of ["createOffer", "createAnswer"]) {
|
|
2478
|
+
const original = pc[method].bind(pc);
|
|
2479
|
+
pc[method] = async (...args) => {
|
|
2480
|
+
trimVideoCodecs(pc);
|
|
2481
|
+
const desc = await original(...args);
|
|
2482
|
+
const bytes = new TextEncoder().encode(desc.sdp || "").length;
|
|
2483
|
+
console[bytes > SDP_BUDGET ? "warn" : "log"](
|
|
2484
|
+
`[call] ${method} SDP ${bytes}B / ${SDP_BUDGET}B budget` + (bytes > SDP_BUDGET ? " \u2014 too large for the Carrier invite channel, falling back to the offline path" : "")
|
|
2485
|
+
);
|
|
2486
|
+
return desc;
|
|
2487
|
+
};
|
|
2488
|
+
}
|
|
2489
|
+
return pc;
|
|
2490
|
+
}
|
|
2457
2491
|
function useCallController(selfId, onCallLog) {
|
|
2458
2492
|
const [incoming, setIncoming] = React.useState(null);
|
|
2459
2493
|
const [active, setActive] = React.useState(null);
|
|
@@ -2476,7 +2510,7 @@ function useCallController(selfId, onCallLog) {
|
|
|
2476
2510
|
const engine = new CallEngine({
|
|
2477
2511
|
selfId,
|
|
2478
2512
|
signaling,
|
|
2479
|
-
createPeerConnection: (c) =>
|
|
2513
|
+
createPeerConnection: (c) => createLeanPeerConnection(c),
|
|
2480
2514
|
getLocalMedia: (k) => navigator.mediaDevices.getUserMedia({ audio: k.audio, video: k.video }),
|
|
2481
2515
|
getDisplayMedia: (c) => navigator.mediaDevices.getDisplayMedia(c || { video: true, audio: false }),
|
|
2482
2516
|
iceServers: CALL_ICE_SERVERS,
|
|
@@ -2688,6 +2722,7 @@ function CallOverlay({ T, ctl, peers }) {
|
|
|
2688
2722
|
{ key: "local", stream: ctl.localStream, label: ctl.sharing ? T && T.yourScreen || "Your screen" : T && T.you || "You", mirror: !ctl.sharing }
|
|
2689
2723
|
].filter((t) => streamHasVideo(t.stream));
|
|
2690
2724
|
const feat = tiles.find((t) => t.key === featured) || tiles[0] || { key: "remote", stream: ctl.remoteStream, label: name };
|
|
2725
|
+
const others = tiles.filter((t) => t.key !== feat.key);
|
|
2691
2726
|
return (
|
|
2692
2727
|
// The thumbnail strip and the controls FLOAT over the video instead of
|
|
2693
2728
|
// stacking under it. A phone camera is 9:16, so the featured tile is
|
|
@@ -2708,11 +2743,12 @@ function CallOverlay({ T, ctl, peers }) {
|
|
|
2708
2743
|
gap: 12,
|
|
2709
2744
|
background: "linear-gradient(to top, rgba(0,0,0,.65), rgba(0,0,0,0))",
|
|
2710
2745
|
pointerEvents: "none"
|
|
2711
|
-
} },
|
|
2746
|
+
} }, others.length > 0 && /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: 12, justifyContent: "center", flexWrap: "wrap", pointerEvents: "auto" } }, others.map((t) => /* @__PURE__ */ React.createElement(
|
|
2712
2747
|
"button",
|
|
2713
2748
|
{
|
|
2714
2749
|
key: t.key,
|
|
2715
2750
|
onClick: () => setFeatured(t.key),
|
|
2751
|
+
title: `Show ${t.label}`,
|
|
2716
2752
|
style: {
|
|
2717
2753
|
position: "relative",
|
|
2718
2754
|
width: 150,
|
|
@@ -2722,7 +2758,7 @@ function CallOverlay({ T, ctl, peers }) {
|
|
|
2722
2758
|
background: "#000",
|
|
2723
2759
|
cursor: "pointer",
|
|
2724
2760
|
padding: 0,
|
|
2725
|
-
border:
|
|
2761
|
+
border: "1px solid var(--line)"
|
|
2726
2762
|
}
|
|
2727
2763
|
},
|
|
2728
2764
|
/* @__PURE__ */ React.createElement(VideoSurface, { stream: t.stream, muted: true, style: { width: "100%", height: "100%", objectFit: "contain", transform: t.mirror ? "scaleX(-1)" : "none" } }),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/lan",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.256",
|
|
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",
|