@decentnetwork/lan 0.1.166 → 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.
- package/bin/tun-helper-darwin-amd64 +0 -0
- package/bin/tun-helper-darwin-arm64 +0 -0
- package/bin/tun-helper-linux-amd64 +0 -0
- package/bin/tun-helper-linux-arm64 +0 -0
- package/dist/carrier/offline-call.d.ts +69 -0
- package/dist/carrier/offline-call.js +192 -0
- package/dist/carrier/peer-manager.d.ts +13 -3
- package/dist/carrier/peer-manager.js +57 -4
- package/dist/daemon/server.js +28 -4
- package/dist/types.d.ts +17 -0
- package/dist/ui/desktop/app.js +31 -12
- package/dist/ui/desktop/vendor/peer-webrtc.js +374 -20
- package/dist/ui/server.js +6 -4
- package/package.json +4 -3
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { type RtcSignal } from "@decentnetwork/peer-webrtc";
|
|
3
|
+
/**
|
|
4
|
+
* Configuration for the offline-call fallback. Defaults target the public
|
|
5
|
+
* Beagle gateways; the appKey/appName must match what the target device
|
|
6
|
+
* registered with (see the Beagle app's Service.swift).
|
|
7
|
+
*/
|
|
8
|
+
export interface OfflineCallConfig {
|
|
9
|
+
/** Master switch. When false the bridge is inert. */
|
|
10
|
+
enabled: boolean;
|
|
11
|
+
/** Beagle push appKey (matches the callee's registration). */
|
|
12
|
+
appKey: string;
|
|
13
|
+
/** Beagle push appName — the callee app's CFBundleName ("Beagle"/"webrtc"). */
|
|
14
|
+
appName: string;
|
|
15
|
+
/** Socket.IO signaling gateway (primary). */
|
|
16
|
+
socketUrl: string;
|
|
17
|
+
/** Ordered socket.IO gateways to try — connects to the first that comes up,
|
|
18
|
+
* switches to the next on failure. Defaults to [socketUrl]. Both call peers
|
|
19
|
+
* must agree on a server, so keep this list in sync with the Beagle apps. */
|
|
20
|
+
socketUrls?: string[];
|
|
21
|
+
/** Push-API endpoints (POST /push-api/push-message), tried in order. */
|
|
22
|
+
pushEndpoints?: string[];
|
|
23
|
+
}
|
|
24
|
+
export declare const DEFAULT_OFFLINE_CONFIG: Omit<OfflineCallConfig, "enabled">;
|
|
25
|
+
/**
|
|
26
|
+
* Offline-call bridge — the socket.io + push path Beagle uses to reach a peer
|
|
27
|
+
* whose app is backgrounded/offline (so a live Carrier session doesn't exist).
|
|
28
|
+
*
|
|
29
|
+
* The daemon uses this as a FALLBACK: `sendCallSignal` tries Carrier first and
|
|
30
|
+
* drops to this only when there's no live session. Signaling then rides the
|
|
31
|
+
* socket.io room (keyed by the RtcSignal `callId`); the initial offer also fires
|
|
32
|
+
* a push to wake the callee. Inbound socket.io signals are re-emitted as
|
|
33
|
+
* `"signal"` events so the daemon forwards them to the browser exactly like
|
|
34
|
+
* Carrier invites.
|
|
35
|
+
*
|
|
36
|
+
* Runs entirely in the daemon (Node): socket.io-client for signaling, fetch for
|
|
37
|
+
* push. socket.io-client is an OPTIONAL dependency — if it isn't installed the
|
|
38
|
+
* bridge stays disabled and logs a hint rather than crashing.
|
|
39
|
+
*/
|
|
40
|
+
export declare class OfflineCallBridge extends EventEmitter {
|
|
41
|
+
#private;
|
|
42
|
+
constructor(opts: {
|
|
43
|
+
config: OfflineCallConfig;
|
|
44
|
+
selfUserId: string;
|
|
45
|
+
selfName: string;
|
|
46
|
+
log?: (msg: string) => void;
|
|
47
|
+
});
|
|
48
|
+
get enabled(): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Send a call signal to `userid` over the offline path. Decodes the signal to
|
|
51
|
+
* key the room by `callId`; for the initial offer it also pushes to wake the
|
|
52
|
+
* callee. Throws if the bridge can't be brought up (caller keeps the original
|
|
53
|
+
* Carrier error).
|
|
54
|
+
*/
|
|
55
|
+
/** Extract the callId from a raw signal payload (undefined if not decodable). */
|
|
56
|
+
callIdOf(data: Uint8Array | string): string | undefined;
|
|
57
|
+
/** True once this call has fallen back to the offline path. */
|
|
58
|
+
isOfflineCall(callId: string): boolean;
|
|
59
|
+
/** Mark a call as committed to the Carrier (online) path. */
|
|
60
|
+
markOnlineCall(callId: string): void;
|
|
61
|
+
/** True once this call has committed to the Carrier path. */
|
|
62
|
+
isOnlineCall(callId: string): boolean;
|
|
63
|
+
/** Drop a finished call's transport state (best-effort cleanup). */
|
|
64
|
+
forgetCall(callId: string): void;
|
|
65
|
+
sendSignal(userid: string, data: Uint8Array | string, hasVideo: boolean): Promise<void>;
|
|
66
|
+
/** Tear down the socket connection (e.g. on daemon shutdown). */
|
|
67
|
+
close(): void;
|
|
68
|
+
}
|
|
69
|
+
export type { RtcSignal };
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { SocketIoSignaling, BeaglePushClient, decodeSignal, encodeSignalBytes } from "@decentnetwork/peer-webrtc";
|
|
3
|
+
export const DEFAULT_OFFLINE_CONFIG = {
|
|
4
|
+
// From the Beagle app (Service.swift). Overridable via config.
|
|
5
|
+
appKey: "08726e938c624c4da9dcd9a85f2bbcd8",
|
|
6
|
+
appName: "Beagle",
|
|
7
|
+
socketUrl: "https://io.beagle.chat",
|
|
8
|
+
socketUrls: ["https://io.beagle.chat"],
|
|
9
|
+
pushEndpoints: [
|
|
10
|
+
"https://pushapi.beagle.chat/push-api/push-message",
|
|
11
|
+
"https://www.callpass.cn/push-api/push-message",
|
|
12
|
+
"https://tokyo.fi.chat:3004/push-api/push-message"
|
|
13
|
+
]
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Offline-call bridge — the socket.io + push path Beagle uses to reach a peer
|
|
17
|
+
* whose app is backgrounded/offline (so a live Carrier session doesn't exist).
|
|
18
|
+
*
|
|
19
|
+
* The daemon uses this as a FALLBACK: `sendCallSignal` tries Carrier first and
|
|
20
|
+
* drops to this only when there's no live session. Signaling then rides the
|
|
21
|
+
* socket.io room (keyed by the RtcSignal `callId`); the initial offer also fires
|
|
22
|
+
* a push to wake the callee. Inbound socket.io signals are re-emitted as
|
|
23
|
+
* `"signal"` events so the daemon forwards them to the browser exactly like
|
|
24
|
+
* Carrier invites.
|
|
25
|
+
*
|
|
26
|
+
* Runs entirely in the daemon (Node): socket.io-client for signaling, fetch for
|
|
27
|
+
* push. socket.io-client is an OPTIONAL dependency — if it isn't installed the
|
|
28
|
+
* bridge stays disabled and logs a hint rather than crashing.
|
|
29
|
+
*/
|
|
30
|
+
export class OfflineCallBridge extends EventEmitter {
|
|
31
|
+
#cfg;
|
|
32
|
+
#selfUserId;
|
|
33
|
+
#selfName;
|
|
34
|
+
#log;
|
|
35
|
+
#push;
|
|
36
|
+
#signaling;
|
|
37
|
+
#socket;
|
|
38
|
+
#connecting;
|
|
39
|
+
/** callIds that have fallen to the offline path — subsequent signals for them
|
|
40
|
+
* skip the Carrier retry entirely (no per-signal 1.5s stall). */
|
|
41
|
+
#offlineCalls = new Set();
|
|
42
|
+
/** callIds committed to the Carrier (online) path — a later transient session
|
|
43
|
+
* flap must NOT divert their signals to socket.io (splitting transports
|
|
44
|
+
* mid-call breaks ICE: the peer gets candidates over two channels). */
|
|
45
|
+
#onlineCalls = new Set();
|
|
46
|
+
constructor(opts) {
|
|
47
|
+
super();
|
|
48
|
+
this.#cfg = opts.config;
|
|
49
|
+
this.#selfUserId = opts.selfUserId;
|
|
50
|
+
this.#selfName = opts.selfName;
|
|
51
|
+
this.#log = opts.log ?? (() => { });
|
|
52
|
+
}
|
|
53
|
+
get enabled() {
|
|
54
|
+
return this.#cfg.enabled;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Send a call signal to `userid` over the offline path. Decodes the signal to
|
|
58
|
+
* key the room by `callId`; for the initial offer it also pushes to wake the
|
|
59
|
+
* callee. Throws if the bridge can't be brought up (caller keeps the original
|
|
60
|
+
* Carrier error).
|
|
61
|
+
*/
|
|
62
|
+
/** Extract the callId from a raw signal payload (undefined if not decodable). */
|
|
63
|
+
callIdOf(data) {
|
|
64
|
+
try {
|
|
65
|
+
return decodeSignal(data).callId || undefined;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** True once this call has fallen back to the offline path. */
|
|
72
|
+
isOfflineCall(callId) {
|
|
73
|
+
return this.#offlineCalls.has(callId);
|
|
74
|
+
}
|
|
75
|
+
/** Mark a call as committed to the Carrier (online) path. */
|
|
76
|
+
markOnlineCall(callId) {
|
|
77
|
+
this.#onlineCalls.add(callId);
|
|
78
|
+
}
|
|
79
|
+
/** True once this call has committed to the Carrier path. */
|
|
80
|
+
isOnlineCall(callId) {
|
|
81
|
+
return this.#onlineCalls.has(callId);
|
|
82
|
+
}
|
|
83
|
+
/** Drop a finished call's transport state (best-effort cleanup). */
|
|
84
|
+
forgetCall(callId) {
|
|
85
|
+
this.#offlineCalls.delete(callId);
|
|
86
|
+
this.#onlineCalls.delete(callId);
|
|
87
|
+
}
|
|
88
|
+
async sendSignal(userid, data, hasVideo) {
|
|
89
|
+
if (!this.#cfg.enabled)
|
|
90
|
+
throw new Error("offline calls disabled");
|
|
91
|
+
const signal = decodeSignal(data);
|
|
92
|
+
if (!signal.callId)
|
|
93
|
+
throw new Error("offline call signal has no callId");
|
|
94
|
+
this.#offlineCalls.add(signal.callId);
|
|
95
|
+
await this.#ensureConnected();
|
|
96
|
+
// Ring the callee for the first outbound signal of a call (the offer).
|
|
97
|
+
if (signal.type === "offer" && this.#push) {
|
|
98
|
+
const woke = await this.#push
|
|
99
|
+
.ring(userid, {
|
|
100
|
+
callerUserId: this.#selfUserId,
|
|
101
|
+
callId: signal.callId,
|
|
102
|
+
callerName: this.#selfName,
|
|
103
|
+
hasVideo,
|
|
104
|
+
channel: "socketio"
|
|
105
|
+
})
|
|
106
|
+
.catch(() => false);
|
|
107
|
+
this.#log(`offline call: push ${woke ? "sent" : "FAILED"} to ${userid} (call ${signal.callId})`);
|
|
108
|
+
}
|
|
109
|
+
this.#signaling.joinCall(signal.callId);
|
|
110
|
+
const kind = signal.type === "candidate"
|
|
111
|
+
? `candidate(${signal.candidates?.length ?? 0})`
|
|
112
|
+
: signal.type === "event" ? `event:${signal.event}` : signal.type;
|
|
113
|
+
this.#log(`offline call: SEND ${kind} to ${userid} (call ${signal.callId})`);
|
|
114
|
+
this.#signaling.send(userid, signal);
|
|
115
|
+
}
|
|
116
|
+
/** Bring up the socket.io connection + signaling/push clients once. */
|
|
117
|
+
async #ensureConnected() {
|
|
118
|
+
if (this.#signaling)
|
|
119
|
+
return;
|
|
120
|
+
if (this.#connecting)
|
|
121
|
+
return this.#connecting;
|
|
122
|
+
this.#connecting = this.#connect();
|
|
123
|
+
try {
|
|
124
|
+
await this.#connecting;
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
this.#connecting = undefined;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async #connect() {
|
|
131
|
+
let io;
|
|
132
|
+
try {
|
|
133
|
+
// Optional dependency — imported lazily so the daemon runs without it.
|
|
134
|
+
const mod = (await import("socket.io-client"));
|
|
135
|
+
io = mod.io;
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
throw new Error("offline calls need socket.io-client — run `npm install socket.io-client` in decentlan");
|
|
139
|
+
}
|
|
140
|
+
// Try each gateway in order; connect to the first that comes up.
|
|
141
|
+
const urls = this.#cfg.socketUrls?.length ? this.#cfg.socketUrls : [this.#cfg.socketUrl];
|
|
142
|
+
let connectedUrl;
|
|
143
|
+
for (const url of urls) {
|
|
144
|
+
const sock = io(url, { transports: ["websocket"], forceNew: true, reconnection: false });
|
|
145
|
+
const ok = await new Promise((resolve) => {
|
|
146
|
+
const t = setTimeout(() => resolve(false), 7000);
|
|
147
|
+
sock.on("connect", () => { clearTimeout(t); resolve(true); });
|
|
148
|
+
sock.on("connect_error", () => { clearTimeout(t); resolve(false); });
|
|
149
|
+
});
|
|
150
|
+
if (ok) {
|
|
151
|
+
this.#socket = sock;
|
|
152
|
+
connectedUrl = url;
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
this.#log(`offline call: socket gateway ${url} unreachable — trying next`);
|
|
156
|
+
try {
|
|
157
|
+
sock.disconnect?.();
|
|
158
|
+
}
|
|
159
|
+
catch { /* best-effort */ }
|
|
160
|
+
}
|
|
161
|
+
if (!this.#socket)
|
|
162
|
+
throw new Error(`offline calls: no reachable socket gateway (${urls.join(", ")})`);
|
|
163
|
+
this.#signaling = new SocketIoSignaling(this.#socket, { userId: this.#selfUserId });
|
|
164
|
+
this.#signaling.onSignal((peerId, signal) => {
|
|
165
|
+
// Re-emit inbound socket.io signals just like a Carrier invite so the
|
|
166
|
+
// daemon's existing call-signal queue forwards them to the browser.
|
|
167
|
+
const kind = signal.type === "candidate"
|
|
168
|
+
? `candidate(${signal.candidates?.length ?? 0})`
|
|
169
|
+
: signal.type === "event" ? `event:${signal.event}` : signal.type;
|
|
170
|
+
this.#log(`offline call: RECV ${kind} from ${peerId} (call ${signal.callId ?? "?"})`);
|
|
171
|
+
this.emit("signal", { userid: peerId, data: encodeSignalBytes(signal) });
|
|
172
|
+
});
|
|
173
|
+
this.#push = new BeaglePushClient({
|
|
174
|
+
appKey: this.#cfg.appKey,
|
|
175
|
+
appName: this.#cfg.appName,
|
|
176
|
+
endpoints: this.#cfg.pushEndpoints
|
|
177
|
+
});
|
|
178
|
+
this.#log(`offline call bridge connected to ${connectedUrl}`);
|
|
179
|
+
}
|
|
180
|
+
/** Tear down the socket connection (e.g. on daemon shutdown). */
|
|
181
|
+
close() {
|
|
182
|
+
try {
|
|
183
|
+
this.#socket?.disconnect?.();
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
/* best-effort */
|
|
187
|
+
}
|
|
188
|
+
this.#socket = undefined;
|
|
189
|
+
this.#signaling = undefined;
|
|
190
|
+
this.#push = undefined;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { Peer } from "@decentnetwork/peer";
|
|
6
6
|
import { EventEmitter } from "events";
|
|
7
|
+
import { type OfflineCallConfig } from "./offline-call.js";
|
|
7
8
|
import { PacketSession } from "./packet-session.js";
|
|
8
9
|
import type { PeerIdentity, RemotePeer } from "./types.js";
|
|
9
10
|
import type { BootstrapNode } from "../types.js";
|
|
@@ -21,6 +22,10 @@ export interface PeerManagerOptions {
|
|
|
21
22
|
/** Directory for persisting partially-received files so a dropped/restarted
|
|
22
23
|
* transfer resumes instead of restarting (断点续传). */
|
|
23
24
|
fileResumeDir?: string;
|
|
25
|
+
/** Offline-call fallback (socket.io + push). When enabled, a call to a peer
|
|
26
|
+
* with no live Carrier session rings it via the Beagle push gateway and
|
|
27
|
+
* signals over socket.io instead of failing. */
|
|
28
|
+
offlineCall?: OfflineCallConfig;
|
|
24
29
|
}
|
|
25
30
|
export declare class PeerManager extends EventEmitter {
|
|
26
31
|
private peer;
|
|
@@ -28,6 +33,9 @@ export declare class PeerManager extends EventEmitter {
|
|
|
28
33
|
private sessions;
|
|
29
34
|
private sessionCounter;
|
|
30
35
|
private logger;
|
|
36
|
+
private offlineBridge;
|
|
37
|
+
private offlineConfig;
|
|
38
|
+
private nickname;
|
|
31
39
|
constructor();
|
|
32
40
|
create(opts: PeerManagerOptions): Promise<void>;
|
|
33
41
|
start(): Promise<void>;
|
|
@@ -70,10 +78,12 @@ export declare class PeerManager extends EventEmitter {
|
|
|
70
78
|
* Send a WebRTC call-signaling payload to a friend over the Carrier
|
|
71
79
|
* friend-invite channel (the "carrier" extension), the exact transport the
|
|
72
80
|
* iOS/Android Beagle apps listen on for calls. `data` is an RtcSignal JSON
|
|
73
|
-
* string/bytes (see @decentnetwork/peer-webrtc). Realtime
|
|
74
|
-
* session
|
|
81
|
+
* string/bytes (see @decentnetwork/peer-webrtc). Realtime over Carrier when a
|
|
82
|
+
* live session exists; otherwise falls back to the offline path (socket.io +
|
|
83
|
+
* push) when configured, so a call to a backgrounded/offline phone still
|
|
84
|
+
* rings instead of failing.
|
|
75
85
|
*/
|
|
76
|
-
sendCallSignal(userid: string, data: Uint8Array | string): Promise<void>;
|
|
86
|
+
sendCallSignal(userid: string, data: Uint8Array | string, hasVideo?: boolean): Promise<void>;
|
|
77
87
|
/** True when the friend is a NATIVE Carrier client (iOS/Android/C): its
|
|
78
88
|
* relay/DHT key differs from its identity key. JS peers announce their
|
|
79
89
|
* identity key as their DHT key, so the two match. */
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { Peer } from "@decentnetwork/peer";
|
|
6
6
|
import { EventEmitter } from "events";
|
|
7
|
+
import { OfflineCallBridge } from "./offline-call.js";
|
|
7
8
|
import { PacketSession } from "./packet-session.js";
|
|
8
9
|
import { FrameCodec } from "./frame.js";
|
|
9
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";
|
|
@@ -14,12 +15,17 @@ export class PeerManager extends EventEmitter {
|
|
|
14
15
|
sessions = new Map();
|
|
15
16
|
sessionCounter = 1;
|
|
16
17
|
logger;
|
|
18
|
+
offlineBridge = null;
|
|
19
|
+
offlineConfig = null;
|
|
20
|
+
nickname = "";
|
|
17
21
|
constructor() {
|
|
18
22
|
super();
|
|
19
23
|
this.logger = new Logger({ prefix: "PeerManager" });
|
|
20
24
|
}
|
|
21
25
|
async create(opts) {
|
|
22
26
|
this.logger.info("Creating Peer instance");
|
|
27
|
+
this.offlineConfig = opts.offlineCall ?? null;
|
|
28
|
+
this.nickname = opts.nickname ?? "";
|
|
23
29
|
this.peer = await Peer.create({
|
|
24
30
|
keyFile: opts.keyFile,
|
|
25
31
|
compatibilityMode: "legacy",
|
|
@@ -50,6 +56,21 @@ export class PeerManager extends EventEmitter {
|
|
|
50
56
|
userid: this.peer.userid(),
|
|
51
57
|
};
|
|
52
58
|
this.logger.info(`Peer started: ${this.identity.address}`);
|
|
59
|
+
// Bring up the offline-call fallback (socket.io + push) if configured.
|
|
60
|
+
// Signals it receives are re-emitted as "call-signal" — identical to a
|
|
61
|
+
// Carrier invite — so the daemon's existing browser bridge handles them.
|
|
62
|
+
if (this.offlineConfig?.enabled) {
|
|
63
|
+
this.offlineBridge = new OfflineCallBridge({
|
|
64
|
+
config: this.offlineConfig,
|
|
65
|
+
selfUserId: this.identity.userid,
|
|
66
|
+
selfName: this.nickname || this.identity.userid,
|
|
67
|
+
log: (m) => this.logger.info(m),
|
|
68
|
+
});
|
|
69
|
+
this.offlineBridge.on("signal", (evt) => {
|
|
70
|
+
this.emit("call-signal", { pubkey: evt.userid, data: evt.data });
|
|
71
|
+
});
|
|
72
|
+
this.logger.info("Offline-call fallback enabled (socket.io + push)");
|
|
73
|
+
}
|
|
53
74
|
}
|
|
54
75
|
async joinNetwork() {
|
|
55
76
|
if (!this.peer) {
|
|
@@ -133,13 +154,45 @@ export class PeerManager extends EventEmitter {
|
|
|
133
154
|
* Send a WebRTC call-signaling payload to a friend over the Carrier
|
|
134
155
|
* friend-invite channel (the "carrier" extension), the exact transport the
|
|
135
156
|
* iOS/Android Beagle apps listen on for calls. `data` is an RtcSignal JSON
|
|
136
|
-
* string/bytes (see @decentnetwork/peer-webrtc). Realtime
|
|
137
|
-
* session
|
|
157
|
+
* string/bytes (see @decentnetwork/peer-webrtc). Realtime over Carrier when a
|
|
158
|
+
* live session exists; otherwise falls back to the offline path (socket.io +
|
|
159
|
+
* push) when configured, so a call to a backgrounded/offline phone still
|
|
160
|
+
* rings instead of failing.
|
|
138
161
|
*/
|
|
139
|
-
async sendCallSignal(userid, data) {
|
|
162
|
+
async sendCallSignal(userid, data, hasVideo = true) {
|
|
140
163
|
if (!this.peer)
|
|
141
164
|
throw new Error("Peer not created. Call create() first.");
|
|
142
|
-
|
|
165
|
+
// Once a call has fallen to the offline path, keep every later signal for it
|
|
166
|
+
// on that path — don't re-attempt Carrier per signal (that stalled the UI
|
|
167
|
+
// ~1.5s each and reordered signals across transports).
|
|
168
|
+
const callId = this.offlineBridge?.enabled ? this.offlineBridge.callIdOf(data) : undefined;
|
|
169
|
+
if (callId && this.offlineBridge.isOfflineCall(callId)) {
|
|
170
|
+
await this.offlineBridge.sendSignal(userid, data, hasVideo);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
// Fail fast (1.5s, not 5s) so an offline peer drops to push quickly.
|
|
175
|
+
await this.peer.sendInvite(userid, data, { ext: "carrier", establishTimeoutMs: 1500 });
|
|
176
|
+
// Committed to Carrier — keep this call there even if it briefly flaps.
|
|
177
|
+
if (callId)
|
|
178
|
+
this.offlineBridge?.markOnlineCall(callId);
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
// A call already committed to Carrier must NOT split to socket.io on a
|
|
182
|
+
// transient flap (it breaks ICE). Drop this signal; ICE recovers via the
|
|
183
|
+
// other candidates / a retried offer.
|
|
184
|
+
if (callId && this.offlineBridge?.isOnlineCall(callId)) {
|
|
185
|
+
this.logger.info(`sendCallSignal: dropped a Carrier signal for online call ${callId} (${err.message})`);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
// No live session and not yet committed to Carrier — use the offline path.
|
|
189
|
+
if (this.offlineBridge?.enabled) {
|
|
190
|
+
this.logger.info(`sendCallSignal: Carrier failed (${err.message}) — offline path to ${userid}`);
|
|
191
|
+
await this.offlineBridge.sendSignal(userid, data, hasVideo);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
throw err;
|
|
195
|
+
}
|
|
143
196
|
}
|
|
144
197
|
/** True when the friend is a NATIVE Carrier client (iOS/Android/C): its
|
|
145
198
|
* relay/DHT key differs from its identity key. JS peers announce their
|
package/dist/daemon/server.js
CHANGED
|
@@ -5,6 +5,7 @@ import { resolve } from "path";
|
|
|
5
5
|
import { EventEmitter } from "events";
|
|
6
6
|
import { existsSync, readFileSync, writeFileSync, unlinkSync } from "fs";
|
|
7
7
|
import { PeerManager } from "../carrier/peer-manager.js";
|
|
8
|
+
import { DEFAULT_OFFLINE_CONFIG } from "../carrier/offline-call.js";
|
|
8
9
|
import { TunDevice } from "../tun/tun-device.js";
|
|
9
10
|
import { RouteManager } from "../tun/route-manager.js";
|
|
10
11
|
import { Ipam } from "../ipam/ipam.js";
|
|
@@ -292,6 +293,18 @@ export class DaemonServer {
|
|
|
292
293
|
// Persist partially-received files so a dropped/restarted transfer
|
|
293
294
|
// resumes instead of restarting from 0 (断点续传).
|
|
294
295
|
fileResumeDir: resolve(this.configDir, "downloads", ".partial"),
|
|
296
|
+
// Offline-call fallback (socket.io + push): ring an offline/backgrounded
|
|
297
|
+
// phone via the Beagle push gateway when there's no live Carrier
|
|
298
|
+
// session. Enabled by default with the public Beagle gateways; override
|
|
299
|
+
// appKey/appName/socketUrl under `call:` in config.yaml.
|
|
300
|
+
offlineCall: {
|
|
301
|
+
enabled: this.config.call?.offlineEnabled ?? true,
|
|
302
|
+
appKey: this.config.call?.appKey ?? DEFAULT_OFFLINE_CONFIG.appKey,
|
|
303
|
+
appName: this.config.call?.appName ?? DEFAULT_OFFLINE_CONFIG.appName,
|
|
304
|
+
socketUrl: this.config.call?.socketUrl ?? DEFAULT_OFFLINE_CONFIG.socketUrl,
|
|
305
|
+
socketUrls: this.config.call?.socketUrls ?? DEFAULT_OFFLINE_CONFIG.socketUrls,
|
|
306
|
+
pushEndpoints: this.config.call?.pushEndpoints ?? DEFAULT_OFFLINE_CONFIG.pushEndpoints,
|
|
307
|
+
},
|
|
295
308
|
});
|
|
296
309
|
await this.peerManager.start();
|
|
297
310
|
this.logger.info(`Identity: ${this.peerManager.getAddress()}`);
|
|
@@ -467,10 +480,12 @@ export class DaemonServer {
|
|
|
467
480
|
// Beagle apps themselves use. Detected via the DHT-key signature.
|
|
468
481
|
if (this.peerManager?.isNativeFriend(userid)) {
|
|
469
482
|
// The FileModel JSON envelope is base64 (inflates ~1.34x) + a little
|
|
470
|
-
// JSON overhead, and the whole thing must fit the
|
|
471
|
-
// (CARRIER_MAX_APP_BULKMSG_LEN
|
|
472
|
-
//
|
|
473
|
-
|
|
483
|
+
// JSON overhead, and the whole thing must fit the 16MB bulkmsg cap
|
|
484
|
+
// (CARRIER_MAX_APP_BULKMSG_LEN, raised from 5MB alongside the receive
|
|
485
|
+
// reorder buffer in peer >= 0.1.87). 11MB raw → ~14.7MB envelope,
|
|
486
|
+
// safely under 16MB. Needs BOTH ends on the raised cap. (Very large /
|
|
487
|
+
// streaming transfers should use toxcore file transfer, not inline.)
|
|
488
|
+
const INLINE_MAX = 11 * 1024 * 1024;
|
|
474
489
|
if (data.length > INLINE_MAX) {
|
|
475
490
|
throw new Error(`This friend is a native (iOS/Android) client — files are sent inline and limited to ` +
|
|
476
491
|
`${(INLINE_MAX / 1024 / 1024).toFixed(1)} MB (this is ${(data.length / 1024 / 1024).toFixed(1)} MB).`);
|
|
@@ -563,6 +578,15 @@ export class DaemonServer {
|
|
|
563
578
|
});
|
|
564
579
|
}
|
|
565
580
|
const signals = this.callSignalQueue.splice(0, this.callSignalQueue.length);
|
|
581
|
+
if (signals.length) {
|
|
582
|
+
const kinds = signals.map((s) => { try {
|
|
583
|
+
return JSON.parse(s.data).type;
|
|
584
|
+
}
|
|
585
|
+
catch {
|
|
586
|
+
return "?";
|
|
587
|
+
} }).join(",");
|
|
588
|
+
this.logger.info(`call-poll -> browser: ${signals.length} signal(s) [${kinds}]`);
|
|
589
|
+
}
|
|
566
590
|
return { signals };
|
|
567
591
|
},
|
|
568
592
|
subscribe: (emit) => {
|
package/dist/types.d.ts
CHANGED
|
@@ -193,6 +193,23 @@ export interface DecentAgentNetConfig {
|
|
|
193
193
|
friends?: FriendsConfig;
|
|
194
194
|
dora?: DoraConfig;
|
|
195
195
|
forwarding?: ForwardingConfig;
|
|
196
|
+
call?: CallConfig;
|
|
197
|
+
}
|
|
198
|
+
export interface CallConfig {
|
|
199
|
+
/** Enable the offline-call fallback (socket.io + push). Default true. When a
|
|
200
|
+
* call target has no live Carrier session, ring it via the Beagle push
|
|
201
|
+
* gateway and signal over socket.io instead of failing. */
|
|
202
|
+
offlineEnabled?: boolean;
|
|
203
|
+
/** Beagle push appKey — must match what the callee's app registered with. */
|
|
204
|
+
appKey?: string;
|
|
205
|
+
/** Beagle push appName (the callee app's CFBundleName, e.g. "Beagle"). */
|
|
206
|
+
appName?: string;
|
|
207
|
+
/** Socket.IO signaling gateway URL (primary). */
|
|
208
|
+
socketUrl?: string;
|
|
209
|
+
/** Ordered socket.IO gateways to try (failover). Defaults to [socketUrl]. */
|
|
210
|
+
socketUrls?: string[];
|
|
211
|
+
/** Push-API endpoints (POST /push-api/push-message), tried in order. */
|
|
212
|
+
pushEndpoints?: string[];
|
|
196
213
|
}
|
|
197
214
|
export interface ForwardingConfig {
|
|
198
215
|
/** Pace + fair-queue egress IP data (default true). Prevents one bursty flow
|
package/dist/ui/desktop/app.js
CHANGED
|
@@ -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) =>
|
|
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);
|
|
@@ -1633,18 +1642,17 @@ function useCallController(selfId) {
|
|
|
1633
1642
|
engineRef.current = null;
|
|
1634
1643
|
};
|
|
1635
1644
|
}, [selfId]);
|
|
1636
|
-
const start = React.useCallback(
|
|
1645
|
+
const start = React.useCallback((peerId, video) => {
|
|
1637
1646
|
const eng = engineRef.current;
|
|
1638
1647
|
if (!eng) {
|
|
1639
1648
|
alert("Calls unavailable (WebRTC not supported here)");
|
|
1640
1649
|
return;
|
|
1641
1650
|
}
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
setActive(
|
|
1645
|
-
} catch (e) {
|
|
1651
|
+
setActive({ callId: null, peerId, video: !!video, direction: "outgoing", state: "ringing" });
|
|
1652
|
+
eng.call(peerId, { audio: true, video: !!video }).then((callId) => setActive((a) => a && a.peerId === peerId && a.callId === null ? { ...a, callId } : a)).catch((e) => {
|
|
1653
|
+
setActive((a) => a && a.peerId === peerId ? null : a);
|
|
1646
1654
|
alert("Could not start call: " + (e && e.message || e));
|
|
1647
|
-
}
|
|
1655
|
+
});
|
|
1648
1656
|
}, []);
|
|
1649
1657
|
const accept = React.useCallback(async () => {
|
|
1650
1658
|
const eng = engineRef.current;
|
|
@@ -1666,7 +1674,7 @@ function useCallController(selfId) {
|
|
|
1666
1674
|
const hangup = React.useCallback(() => {
|
|
1667
1675
|
const eng = engineRef.current;
|
|
1668
1676
|
setActive((a) => {
|
|
1669
|
-
if (eng && a) eng.hangup(a.callId);
|
|
1677
|
+
if (eng && a && a.callId) eng.hangup(a.callId);
|
|
1670
1678
|
return null;
|
|
1671
1679
|
});
|
|
1672
1680
|
setLocalStream(null);
|
|
@@ -1674,13 +1682,23 @@ function useCallController(selfId) {
|
|
|
1674
1682
|
}, []);
|
|
1675
1683
|
const setMuted = React.useCallback((m) => {
|
|
1676
1684
|
const eng = engineRef.current, a = active;
|
|
1677
|
-
if (eng && a) eng.setLocalTrackEnabled(a.callId, "audio", !m);
|
|
1685
|
+
if (eng && a && a.callId) eng.setLocalTrackEnabled(a.callId, "audio", !m);
|
|
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
|
+
}
|
|
1678
1696
|
}, [active]);
|
|
1679
1697
|
const setVideoOn = React.useCallback((v) => {
|
|
1680
1698
|
const eng = engineRef.current, a = active;
|
|
1681
|
-
if (eng && a) eng.setLocalTrackEnabled(a.callId, "video", v);
|
|
1699
|
+
if (eng && a && a.callId) eng.setLocalTrackEnabled(a.callId, "video", v);
|
|
1682
1700
|
}, [active]);
|
|
1683
|
-
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 };
|
|
1684
1702
|
}
|
|
1685
1703
|
function VideoSurface({ stream, muted, style }) {
|
|
1686
1704
|
const ref = React.useCallback((el) => {
|
|
@@ -1790,7 +1808,8 @@ function CallOverlay({ T, ctl, peers }) {
|
|
|
1790
1808
|
setMuted(n);
|
|
1791
1809
|
ctl.setMuted(n);
|
|
1792
1810
|
};
|
|
1793
|
-
const
|
|
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 }));
|
|
1794
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)));
|
|
1795
1814
|
}
|
|
1796
1815
|
Object.assign(window, { useCallController, CallOverlay, IncomingCallModal });
|
|
@@ -28,10 +28,12 @@ var PeerWebRTC = (() => {
|
|
|
28
28
|
// node_modules/@decentnetwork/peer-webrtc/dist/index.js
|
|
29
29
|
var dist_exports = {};
|
|
30
30
|
__export(dist_exports, {
|
|
31
|
+
BeaglePushClient: () => BeaglePushClient,
|
|
31
32
|
BroadcastChannelSignaling: () => BroadcastChannelSignaling,
|
|
32
33
|
CallEngine: () => CallEngine,
|
|
33
34
|
CarrierSignaling: () => CarrierSignaling,
|
|
34
35
|
Emitter: () => Emitter,
|
|
36
|
+
SocketIoSignaling: () => SocketIoSignaling,
|
|
35
37
|
decodeSignal: () => decodeSignal,
|
|
36
38
|
decodeSignalBytesGuard: () => decodeSignalBytesGuard,
|
|
37
39
|
encodeSignal: () => encodeSignal,
|
|
@@ -151,7 +153,7 @@ var PeerWebRTC = (() => {
|
|
|
151
153
|
var DEFAULT_ICE_SERVERS = [
|
|
152
154
|
{ urls: "stun:stun.l.google.com:19302" }
|
|
153
155
|
];
|
|
154
|
-
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;
|
|
155
157
|
var CallEngine = class extends Emitter {
|
|
156
158
|
constructor(opts) {
|
|
157
159
|
var _a;
|
|
@@ -197,17 +199,26 @@ var PeerWebRTC = (() => {
|
|
|
197
199
|
* "remoteStream"/"ended" as the call progresses.
|
|
198
200
|
*/
|
|
199
201
|
async call(peerId, kinds = {}) {
|
|
200
|
-
var _a, _b, _c;
|
|
202
|
+
var _a, _b, _c, _d, _e;
|
|
201
203
|
const audio = (_a = kinds.audio) != null ? _a : true;
|
|
202
204
|
const video = (_b = kinds.video) != null ? _b : false;
|
|
203
205
|
const data = (_c = kinds.data) != null ? _c : false;
|
|
204
206
|
const callId = __privateMethod(this, _CallEngine_instances, newCallId_fn).call(this);
|
|
205
207
|
const session = __privateMethod(this, _CallEngine_instances, createSession_fn).call(this, callId, peerId, "outgoing", { audio, video, data });
|
|
206
208
|
await __privateMethod(this, _CallEngine_instances, attachLocalMedia_fn).call(this, session);
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
209
|
+
if (audio && !((_d = session.localStream) == null ? void 0 : _d.getAudioTracks().length)) {
|
|
210
|
+
try {
|
|
211
|
+
session.pc.addTransceiver("audio", { direction: "recvonly" });
|
|
212
|
+
} catch {
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (video && !((_e = session.localStream) == null ? void 0 : _e.getVideoTracks().length)) {
|
|
216
|
+
try {
|
|
217
|
+
session.pc.addTransceiver("video", { direction: "recvonly" });
|
|
218
|
+
} catch {
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const offer = await session.pc.createOffer();
|
|
211
222
|
await session.pc.setLocalDescription(offer);
|
|
212
223
|
const options = [];
|
|
213
224
|
if (audio)
|
|
@@ -275,6 +286,50 @@ var PeerWebRTC = (() => {
|
|
|
275
286
|
track.enabled = enabled;
|
|
276
287
|
}
|
|
277
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
|
+
}
|
|
278
333
|
/** Tear down every call (e.g. on app shutdown). */
|
|
279
334
|
dispose() {
|
|
280
335
|
for (const session of [...__privateGet(this, _sessions).values()]) {
|
|
@@ -287,6 +342,44 @@ var PeerWebRTC = (() => {
|
|
|
287
342
|
_iceServers = new WeakMap();
|
|
288
343
|
_sessions = new WeakMap();
|
|
289
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
|
+
};
|
|
290
383
|
// ---- internals -------------------------------------------------------
|
|
291
384
|
handleSignal_fn = function(peerId, signal) {
|
|
292
385
|
switch (signal.type) {
|
|
@@ -317,7 +410,10 @@ var PeerWebRTC = (() => {
|
|
|
317
410
|
return;
|
|
318
411
|
const existing = __privateGet(this, _sessions).get(__privateMethod(this, _CallEngine_instances, normId_fn).call(this, signal.callId));
|
|
319
412
|
if (existing) {
|
|
320
|
-
existing.
|
|
413
|
+
if (existing.hasReceivedSdp)
|
|
414
|
+
void __privateMethod(this, _CallEngine_instances, answerRenegotiation_fn).call(this, existing, signal.sdp);
|
|
415
|
+
else
|
|
416
|
+
existing.pendingOffer = signal.sdp;
|
|
321
417
|
return;
|
|
322
418
|
}
|
|
323
419
|
const audio = signal.options ? signal.options.includes("audio") : true;
|
|
@@ -328,15 +424,27 @@ var PeerWebRTC = (() => {
|
|
|
328
424
|
this.emit("incomingCall", __privateGet(this, _toInfo).call(this, session));
|
|
329
425
|
__privateMethod(this, _CallEngine_instances, log_fn).call(this, `incoming call from ${peerId} callId=${signal.callId} audio=${audio} video=${video}`);
|
|
330
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
|
+
};
|
|
331
437
|
onAnswer_fn = function(peerId, signal) {
|
|
332
438
|
const session = __privateGet(this, _sessions).get(__privateMethod(this, _CallEngine_instances, normId_fn).call(this, signal.callId));
|
|
333
|
-
if (!session ||
|
|
439
|
+
if (!session || !signal.sdp)
|
|
334
440
|
return;
|
|
441
|
+
session.peerId = peerId;
|
|
335
442
|
void (async () => {
|
|
336
443
|
try {
|
|
337
444
|
await session.pc.setRemoteDescription({ type: "answer", sdp: signal.sdp });
|
|
338
445
|
session.hasReceivedSdp = true;
|
|
339
|
-
|
|
446
|
+
if (session.state !== "connected")
|
|
447
|
+
__privateMethod(this, _CallEngine_instances, setState_fn).call(this, session, "connecting");
|
|
340
448
|
await __privateMethod(this, _CallEngine_instances, drainRemoteCandidates_fn).call(this, session);
|
|
341
449
|
await __privateMethod(this, _CallEngine_instances, flushLocalCandidates_fn).call(this, session);
|
|
342
450
|
} catch (err) {
|
|
@@ -347,7 +455,7 @@ var PeerWebRTC = (() => {
|
|
|
347
455
|
onCandidate_fn = function(peerId, signal) {
|
|
348
456
|
var _a;
|
|
349
457
|
const session = __privateGet(this, _sessions).get(__privateMethod(this, _CallEngine_instances, normId_fn).call(this, signal.callId));
|
|
350
|
-
if (!session ||
|
|
458
|
+
if (!session || !signal.candidates)
|
|
351
459
|
return;
|
|
352
460
|
for (const c of signal.candidates) {
|
|
353
461
|
const init = {
|
|
@@ -365,7 +473,7 @@ var PeerWebRTC = (() => {
|
|
|
365
473
|
onBye_fn = function(peerId, signal) {
|
|
366
474
|
var _a;
|
|
367
475
|
const session = __privateGet(this, _sessions).get(__privateMethod(this, _CallEngine_instances, normId_fn).call(this, signal.callId));
|
|
368
|
-
if (!session
|
|
476
|
+
if (!session)
|
|
369
477
|
return;
|
|
370
478
|
const reason = (_a = signal.reason) != null ? _a : "normal";
|
|
371
479
|
const state = reason === "busy" ? "busy" : reason === "declined" ? "declined" : "ended";
|
|
@@ -375,7 +483,7 @@ var PeerWebRTC = (() => {
|
|
|
375
483
|
};
|
|
376
484
|
onAuxiliary_fn = function(peerId, signal) {
|
|
377
485
|
const session = __privateGet(this, _sessions).get(__privateMethod(this, _CallEngine_instances, normId_fn).call(this, signal.callId));
|
|
378
|
-
if (!session
|
|
486
|
+
if (!session)
|
|
379
487
|
return;
|
|
380
488
|
if (signal.type === "event") {
|
|
381
489
|
if (signal.event === "ringing")
|
|
@@ -427,12 +535,8 @@ var PeerWebRTC = (() => {
|
|
|
427
535
|
}
|
|
428
536
|
};
|
|
429
537
|
pc.ontrack = (ev) => {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
if (session.remoteStream !== stream) {
|
|
433
|
-
session.remoteStream = stream;
|
|
434
|
-
this.emit("remoteStream", session.callId, stream);
|
|
435
|
-
}
|
|
538
|
+
const stream = __privateMethod(this, _CallEngine_instances, ensureRemoteStream_fn).call(this, session, ev.track);
|
|
539
|
+
this.emit("remoteStream", session.callId, stream);
|
|
436
540
|
};
|
|
437
541
|
pc.oniceconnectionstatechange = () => {
|
|
438
542
|
switch (pc.iceConnectionState) {
|
|
@@ -459,7 +563,8 @@ var PeerWebRTC = (() => {
|
|
|
459
563
|
ensureRemoteStream_fn = function(session, track) {
|
|
460
564
|
if (!session.remoteStream)
|
|
461
565
|
session.remoteStream = new MediaStream();
|
|
462
|
-
session.remoteStream.
|
|
566
|
+
if (!session.remoteStream.getTracks().includes(track))
|
|
567
|
+
session.remoteStream.addTrack(track);
|
|
463
568
|
return session.remoteStream;
|
|
464
569
|
};
|
|
465
570
|
attachLocalMedia_fn = async function(session) {
|
|
@@ -467,7 +572,24 @@ var PeerWebRTC = (() => {
|
|
|
467
572
|
return;
|
|
468
573
|
if (!__privateGet(this, _opts).getLocalMedia || !session.audio && !session.video)
|
|
469
574
|
return;
|
|
470
|
-
|
|
575
|
+
let stream;
|
|
576
|
+
try {
|
|
577
|
+
stream = await __privateGet(this, _opts).getLocalMedia({ audio: session.audio, video: session.video });
|
|
578
|
+
} catch (err) {
|
|
579
|
+
__privateMethod(this, _CallEngine_instances, log_fn).call(this, `getLocalMedia(audio:${session.audio},video:${session.video}) failed: ${err.message}`);
|
|
580
|
+
if (session.video && session.audio) {
|
|
581
|
+
try {
|
|
582
|
+
stream = await __privateGet(this, _opts).getLocalMedia({ audio: true, video: false });
|
|
583
|
+
__privateMethod(this, _CallEngine_instances, log_fn).call(this, `local camera unavailable \u2014 sending audio only, still receiving video, call ${session.callId}`);
|
|
584
|
+
} catch (err2) {
|
|
585
|
+
__privateMethod(this, _CallEngine_instances, log_fn).call(this, `audio-only fallback failed too: ${err2.message}`);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
if (!stream) {
|
|
590
|
+
__privateMethod(this, _CallEngine_instances, log_fn).call(this, `no local media for ${session.callId}; connecting receive-only`);
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
471
593
|
session.localStream = stream;
|
|
472
594
|
for (const track of stream.getTracks()) {
|
|
473
595
|
session.pc.addTrack(track, stream);
|
|
@@ -631,5 +753,237 @@ var PeerWebRTC = (() => {
|
|
|
631
753
|
};
|
|
632
754
|
_peer = new WeakMap();
|
|
633
755
|
_ext = new WeakMap();
|
|
756
|
+
|
|
757
|
+
// node_modules/@decentnetwork/peer-webrtc/dist/signaling/socketio.js
|
|
758
|
+
var _socket, _userId, _handler2, _rooms, _pending, _lastOffer, _wired, _SocketIoSignaling_instances, roomKey_fn, ensureRoom_fn, emitMessage_fn, wire_fn;
|
|
759
|
+
var SocketIoSignaling = class {
|
|
760
|
+
constructor(socket, opts) {
|
|
761
|
+
__privateAdd(this, _SocketIoSignaling_instances);
|
|
762
|
+
__privateAdd(this, _socket);
|
|
763
|
+
__privateAdd(this, _userId);
|
|
764
|
+
__privateAdd(this, _handler2);
|
|
765
|
+
/** Rooms we've asked to join (callId -> confirmed self-membership). */
|
|
766
|
+
__privateAdd(this, _rooms, /* @__PURE__ */ new Map());
|
|
767
|
+
/** Signals queued per room until the server confirms our membership. */
|
|
768
|
+
__privateAdd(this, _pending, /* @__PURE__ */ new Map());
|
|
769
|
+
/** Last offer per callId, RE-SENT when the peer joins the room — the callee
|
|
770
|
+
* (woken by a push) joins seconds after we sent the offer, and socket.io does
|
|
771
|
+
* NOT replay, so without this the peer never sees the offer and never
|
|
772
|
+
* answers (offline call stuck at "connecting"). */
|
|
773
|
+
__privateAdd(this, _lastOffer, /* @__PURE__ */ new Map());
|
|
774
|
+
/** The remote peer id per room, learned from inbound messages so a queued
|
|
775
|
+
* peer-greeting can be addressed. */
|
|
776
|
+
__privateAdd(this, _wired, false);
|
|
777
|
+
__privateSet(this, _socket, socket);
|
|
778
|
+
__privateSet(this, _userId, opts.userId);
|
|
779
|
+
}
|
|
780
|
+
onSignal(handler) {
|
|
781
|
+
__privateSet(this, _handler2, handler);
|
|
782
|
+
__privateMethod(this, _SocketIoSignaling_instances, wire_fn).call(this);
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* Deliver a signal to `peerId`. Joins the call's room (keyed by
|
|
786
|
+
* `signal.callId`) on first use and queues until the server confirms
|
|
787
|
+
* membership, then emits — mirroring the iOS provider, which queues until
|
|
788
|
+
* `connectToRoom`.
|
|
789
|
+
*/
|
|
790
|
+
send(peerId, signal) {
|
|
791
|
+
var _a;
|
|
792
|
+
__privateMethod(this, _SocketIoSignaling_instances, wire_fn).call(this);
|
|
793
|
+
const callId = signal.callId;
|
|
794
|
+
if (!callId) {
|
|
795
|
+
__privateMethod(this, _SocketIoSignaling_instances, emitMessage_fn).call(this, signal);
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
if (signal.type === "offer")
|
|
799
|
+
__privateGet(this, _lastOffer).set(callId, signal);
|
|
800
|
+
const room = __privateMethod(this, _SocketIoSignaling_instances, ensureRoom_fn).call(this, callId);
|
|
801
|
+
if (room.joined) {
|
|
802
|
+
__privateMethod(this, _SocketIoSignaling_instances, emitMessage_fn).call(this, signal);
|
|
803
|
+
} else {
|
|
804
|
+
const q = (_a = __privateGet(this, _pending).get(callId)) != null ? _a : [];
|
|
805
|
+
q.push(signal);
|
|
806
|
+
__privateGet(this, _pending).set(callId, q);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Explicitly join the room for an INCOMING call (e.g. after a push
|
|
811
|
+
* notification delivered the `callId`), so inbound signals for it are
|
|
812
|
+
* received. Safe to call more than once.
|
|
813
|
+
*/
|
|
814
|
+
joinCall(callId) {
|
|
815
|
+
__privateMethod(this, _SocketIoSignaling_instances, wire_fn).call(this);
|
|
816
|
+
__privateMethod(this, _SocketIoSignaling_instances, ensureRoom_fn).call(this, callId);
|
|
817
|
+
}
|
|
818
|
+
/** Leave a call's room (best-effort) and forget its queued state. */
|
|
819
|
+
leaveCall(callId) {
|
|
820
|
+
__privateGet(this, _rooms).delete(callId);
|
|
821
|
+
__privateGet(this, _pending).delete(callId);
|
|
822
|
+
__privateGet(this, _lastOffer).delete(callId);
|
|
823
|
+
__privateGet(this, _socket).emit("leave-channel", { channel: __privateMethod(this, _SocketIoSignaling_instances, roomKey_fn).call(this, callId), sender: __privateGet(this, _userId) });
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
_socket = new WeakMap();
|
|
827
|
+
_userId = new WeakMap();
|
|
828
|
+
_handler2 = new WeakMap();
|
|
829
|
+
_rooms = new WeakMap();
|
|
830
|
+
_pending = new WeakMap();
|
|
831
|
+
_lastOffer = new WeakMap();
|
|
832
|
+
_wired = new WeakMap();
|
|
833
|
+
_SocketIoSignaling_instances = new WeakSet();
|
|
834
|
+
/** The socket.io ROOM key. The native Beagle apps join the room with the
|
|
835
|
+
* call's `UUID.uuidString`, which Swift renders UPPERCASE — so a lowercase
|
|
836
|
+
* `crypto.randomUUID()` from JS would land us in a DIFFERENT room and we'd
|
|
837
|
+
* never receive the peer's answer/candidates. Canonicalize to UPPERCASE to
|
|
838
|
+
* share the native peer's room. (The RtcSignal payload callId stays as-is;
|
|
839
|
+
* the CallEngine matches it case-insensitively.) */
|
|
840
|
+
roomKey_fn = function(callId) {
|
|
841
|
+
return callId.toUpperCase();
|
|
842
|
+
};
|
|
843
|
+
ensureRoom_fn = function(callId) {
|
|
844
|
+
let room = __privateGet(this, _rooms).get(callId);
|
|
845
|
+
if (!room) {
|
|
846
|
+
room = { joined: false };
|
|
847
|
+
__privateGet(this, _rooms).set(callId, room);
|
|
848
|
+
__privateGet(this, _socket).emit("new-channel", { channel: __privateMethod(this, _SocketIoSignaling_instances, roomKey_fn).call(this, callId), sender: __privateGet(this, _userId) });
|
|
849
|
+
}
|
|
850
|
+
return room;
|
|
851
|
+
};
|
|
852
|
+
emitMessage_fn = function(signal) {
|
|
853
|
+
__privateGet(this, _socket).emit("message", { data: encodeSignal(signal), sender: __privateGet(this, _userId) });
|
|
854
|
+
};
|
|
855
|
+
wire_fn = function() {
|
|
856
|
+
if (__privateGet(this, _wired))
|
|
857
|
+
return;
|
|
858
|
+
__privateSet(this, _wired, true);
|
|
859
|
+
__privateGet(this, _socket).on("message", (...args) => {
|
|
860
|
+
var _a;
|
|
861
|
+
const payload = firstObject(args);
|
|
862
|
+
if (!(payload == null ? void 0 : payload.data) || !payload.sender)
|
|
863
|
+
return;
|
|
864
|
+
if (payload.sender === __privateGet(this, _userId))
|
|
865
|
+
return;
|
|
866
|
+
const signal = decodeSignalBytesGuard(payload.data);
|
|
867
|
+
if (signal)
|
|
868
|
+
(_a = __privateGet(this, _handler2)) == null ? void 0 : _a.call(this, payload.sender, signal);
|
|
869
|
+
});
|
|
870
|
+
__privateGet(this, _socket).on("connectToRoom", (...args) => {
|
|
871
|
+
const payload = firstObject(args);
|
|
872
|
+
const sender = payload == null ? void 0 : payload.sender;
|
|
873
|
+
if (!sender)
|
|
874
|
+
return;
|
|
875
|
+
if (sender === __privateGet(this, _userId)) {
|
|
876
|
+
for (const [callId, room] of __privateGet(this, _rooms)) {
|
|
877
|
+
room.joined = true;
|
|
878
|
+
const q = __privateGet(this, _pending).get(callId);
|
|
879
|
+
if (q && q.length) {
|
|
880
|
+
for (const sig of q)
|
|
881
|
+
__privateMethod(this, _SocketIoSignaling_instances, emitMessage_fn).call(this, sig);
|
|
882
|
+
__privateGet(this, _pending).delete(callId);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
} else {
|
|
886
|
+
for (const callId of __privateGet(this, _rooms).keys()) {
|
|
887
|
+
__privateMethod(this, _SocketIoSignaling_instances, emitMessage_fn).call(this, { type: "event", callId, event: "online" });
|
|
888
|
+
const offer = __privateGet(this, _lastOffer).get(callId);
|
|
889
|
+
if (offer)
|
|
890
|
+
__privateMethod(this, _SocketIoSignaling_instances, emitMessage_fn).call(this, offer);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
});
|
|
894
|
+
__privateGet(this, _socket).on("disconnectToRoom", (...args) => {
|
|
895
|
+
const payload = firstObject(args);
|
|
896
|
+
const sender = payload == null ? void 0 : payload.sender;
|
|
897
|
+
if (sender && sender === __privateGet(this, _userId)) {
|
|
898
|
+
for (const room of __privateGet(this, _rooms).values())
|
|
899
|
+
room.joined = false;
|
|
900
|
+
}
|
|
901
|
+
});
|
|
902
|
+
};
|
|
903
|
+
function firstObject(args) {
|
|
904
|
+
const first = args[0];
|
|
905
|
+
return first && typeof first === "object" ? first : void 0;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// node_modules/@decentnetwork/peer-webrtc/dist/signaling/push.js
|
|
909
|
+
var DEFAULT_ENDPOINTS = [
|
|
910
|
+
"https://pushapi.beagle.chat/push-api/push-message",
|
|
911
|
+
"https://www.callpass.cn/push-api/push-message",
|
|
912
|
+
"https://tokyo.fi.chat:3004/push-api/push-message"
|
|
913
|
+
];
|
|
914
|
+
var _appKey, _appName, _endpoints, _fetch;
|
|
915
|
+
var BeaglePushClient = class {
|
|
916
|
+
constructor(opts) {
|
|
917
|
+
__privateAdd(this, _appKey);
|
|
918
|
+
__privateAdd(this, _appName);
|
|
919
|
+
__privateAdd(this, _endpoints);
|
|
920
|
+
__privateAdd(this, _fetch);
|
|
921
|
+
var _a, _b;
|
|
922
|
+
__privateSet(this, _appKey, opts.appKey);
|
|
923
|
+
__privateSet(this, _appName, opts.appName);
|
|
924
|
+
__privateSet(this, _endpoints, ((_a = opts.endpoints) == null ? void 0 : _a.length) ? opts.endpoints : DEFAULT_ENDPOINTS);
|
|
925
|
+
const f = (_b = opts.fetch) != null ? _b : globalThis.fetch;
|
|
926
|
+
if (!f)
|
|
927
|
+
throw new Error("BeaglePushClient: no fetch available \u2014 pass opts.fetch");
|
|
928
|
+
__privateSet(this, _fetch, f);
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* Ring an offline peer. `calleeUserId` is the destination (their Carrier user
|
|
932
|
+
* id); the payload describes the call. Tries each endpoint until one accepts.
|
|
933
|
+
* Resolves `true` on success, `false` if every endpoint failed.
|
|
934
|
+
*/
|
|
935
|
+
async sendCallPush(calleeUserId, payload) {
|
|
936
|
+
var _a;
|
|
937
|
+
const params = new URLSearchParams({
|
|
938
|
+
account: calleeUserId,
|
|
939
|
+
payload: JSON.stringify(payload),
|
|
940
|
+
appKey: __privateGet(this, _appKey),
|
|
941
|
+
appName: __privateGet(this, _appName)
|
|
942
|
+
}).toString();
|
|
943
|
+
for (const endpoint of __privateGet(this, _endpoints)) {
|
|
944
|
+
try {
|
|
945
|
+
const url = endpoint + (endpoint.includes("?") ? "&" : "?") + params;
|
|
946
|
+
const res = await __privateGet(this, _fetch).call(this, url, { method: "POST" });
|
|
947
|
+
if (!res.ok)
|
|
948
|
+
continue;
|
|
949
|
+
const json = await res.json().catch(() => ({}));
|
|
950
|
+
const code = json.code;
|
|
951
|
+
const ok = code === void 0 || code === 0 || code === "0" || ((_a = json.data) == null ? void 0 : _a.success) === true;
|
|
952
|
+
if (ok)
|
|
953
|
+
return true;
|
|
954
|
+
} catch {
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
return false;
|
|
958
|
+
}
|
|
959
|
+
/** Convenience: ring `calleeUserId` for a call over `channel`. */
|
|
960
|
+
ring(calleeUserId, args) {
|
|
961
|
+
var _a;
|
|
962
|
+
return this.sendCallPush(calleeUserId, {
|
|
963
|
+
userId: args.callerUserId,
|
|
964
|
+
hasVideo: args.hasVideo ? "true" : "false",
|
|
965
|
+
callId: args.callId,
|
|
966
|
+
callName: args.callerName,
|
|
967
|
+
action: "call",
|
|
968
|
+
channel: (_a = args.channel) != null ? _a : "socketio"
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
/** Convenience: cancel a ring (e.g. caller hung up before answer). */
|
|
972
|
+
cancel(calleeUserId, args) {
|
|
973
|
+
var _a;
|
|
974
|
+
return this.sendCallPush(calleeUserId, {
|
|
975
|
+
userId: args.callerUserId,
|
|
976
|
+
hasVideo: args.hasVideo ? "true" : "false",
|
|
977
|
+
callId: args.callId,
|
|
978
|
+
callName: args.callerName,
|
|
979
|
+
action: "decline",
|
|
980
|
+
channel: (_a = args.channel) != null ? _a : "socketio"
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
};
|
|
984
|
+
_appKey = new WeakMap();
|
|
985
|
+
_appName = new WeakMap();
|
|
986
|
+
_endpoints = new WeakMap();
|
|
987
|
+
_fetch = new WeakMap();
|
|
634
988
|
return __toCommonJS(dist_exports);
|
|
635
989
|
})();
|
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
|
|
202
|
-
//
|
|
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
|
-
|
|
205
|
-
|
|
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.
|
|
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",
|
|
@@ -79,12 +79,13 @@
|
|
|
79
79
|
},
|
|
80
80
|
"dependencies": {
|
|
81
81
|
"@decentnetwork/dora": "^0.1.13",
|
|
82
|
-
"@decentnetwork/peer": "^0.1.
|
|
83
|
-
"@decentnetwork/peer-webrtc": "^0.
|
|
82
|
+
"@decentnetwork/peer": "^0.1.91",
|
|
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",
|
|
87
87
|
"react": "^18.3.1",
|
|
88
|
+
"socket.io-client": "^4.7.5",
|
|
88
89
|
"yargs": "^17.7.2"
|
|
89
90
|
},
|
|
90
91
|
"devDependencies": {
|