@torrent-tv/proxy 2.2.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +248 -143
- package/bin/cli.js +52 -31
- package/package.json +2 -1
- package/server.js +6 -0
- package/services/data-channel-handler.js +226 -0
- package/services/health-collector.js +48 -0
- package/services/registry-api.js +31 -43
- package/services/tunnel-client.js +105 -29
- package/services/webrtc-manager.js +166 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file WebRTC connection manager for the proxy (Node.js side).
|
|
3
|
+
*
|
|
4
|
+
* Maintains one `PeerConnection` per browser session, keyed by `sessionId`.
|
|
5
|
+
* Receives SDP offers and ICE candidates through the signalling tunnel
|
|
6
|
+
* and responds with SDP answers and its own candidates through the same channel.
|
|
7
|
+
*
|
|
8
|
+
* Data channels for actual streaming are handed off to `data-channel-handler.js`
|
|
9
|
+
* via the `onDataChannel` callback — this module only owns the signalling phase.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import nodeDataChannel from "node-datachannel";
|
|
13
|
+
|
|
14
|
+
/** @import { WebRtcSignal } from './tunnel-client.js' */
|
|
15
|
+
|
|
16
|
+
const ICE_SERVERS = ["stun:stun.l.google.com:19302"];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Configuration for the WebRTC manager.
|
|
20
|
+
*
|
|
21
|
+
* @typedef {Object} WebRtcManagerOptions
|
|
22
|
+
* @property {(sessionId: string, signal: WebRtcSignal) => void} sendSignal
|
|
23
|
+
* Send a WebRTC signal (answer or ICE candidate) back to the browser via
|
|
24
|
+
* the tunnel. Typically wired to `tunnelClient.sendSignal`.
|
|
25
|
+
* @property {(sessionId: string, channel: import("node-datachannel").DataChannel) => void} onDataChannel
|
|
26
|
+
* Called when the browser opens a data channel. Receives the raw
|
|
27
|
+
* `node-datachannel` `DataChannel` object; hand it to `createDataChannelHandler`.
|
|
28
|
+
* @property {(message: string) => void} [onLog]
|
|
29
|
+
* Optional log sink.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The object returned by {@link createWebRtcManager}.
|
|
34
|
+
*
|
|
35
|
+
* @typedef {Object} WebRtcManager
|
|
36
|
+
* @property {(sessionId: string, signal: WebRtcSignal) => void} handleSignal
|
|
37
|
+
* Dispatch an incoming signal (offer / ICE candidate) from the browser to
|
|
38
|
+
* the matching peer connection, creating it if necessary.
|
|
39
|
+
* @property {(sessionId: string) => void} closeSession
|
|
40
|
+
* Tear down and remove a peer connection by session ID.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Create the proxy-side WebRTC connection manager.
|
|
45
|
+
*
|
|
46
|
+
* @param {WebRtcManagerOptions} options
|
|
47
|
+
* @returns {WebRtcManager}
|
|
48
|
+
*/
|
|
49
|
+
export function createWebRtcManager({ sendSignal, onDataChannel, onLog }) {
|
|
50
|
+
/** @type {Map<string, import("node-datachannel").PeerConnection>} */
|
|
51
|
+
const peers = new Map();
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {string} message
|
|
55
|
+
* @returns {void}
|
|
56
|
+
*/
|
|
57
|
+
function log(message) {
|
|
58
|
+
if (typeof onLog === "function") {
|
|
59
|
+
onLog(message);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Retrieve an existing peer connection or create a new one for the session.
|
|
65
|
+
*
|
|
66
|
+
* A new `PeerConnection` is configured with the shared ICE server list and
|
|
67
|
+
* wired up to forward local descriptions and candidates back to the browser.
|
|
68
|
+
*
|
|
69
|
+
* @param {string} sessionId
|
|
70
|
+
* @returns {import("node-datachannel").PeerConnection}
|
|
71
|
+
*/
|
|
72
|
+
function getOrCreatePeer(sessionId) {
|
|
73
|
+
const existing = peers.get(sessionId);
|
|
74
|
+
if (existing) {
|
|
75
|
+
return existing;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const pc = new nodeDataChannel.PeerConnection(`proxy-${sessionId.slice(0, 8)}`, {
|
|
79
|
+
iceServers: ICE_SERVERS
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Forward our ICE candidates to the browser through the tunnel.
|
|
83
|
+
pc.onLocalCandidate((candidate, mid) => {
|
|
84
|
+
sendSignal(sessionId, { type: "candidate", candidate, mid });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Forward our SDP answer to the browser through the tunnel.
|
|
88
|
+
pc.onLocalDescription((sdp, type) => {
|
|
89
|
+
log(`[webrtc] Session ${sessionId.slice(0, 8)}: sending ${type}`);
|
|
90
|
+
sendSignal(sessionId, { type, sdp });
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
pc.onStateChange((state) => {
|
|
94
|
+
log(`[webrtc] Session ${sessionId.slice(0, 8)}: state → ${state}`);
|
|
95
|
+
if (state === "disconnected" || state === "failed" || state === "closed") {
|
|
96
|
+
closeSession(sessionId);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Browser creates the data channel — we receive it here.
|
|
101
|
+
pc.onDataChannel((channel) => {
|
|
102
|
+
log(`[webrtc] Session ${sessionId.slice(0, 8)}: data channel "${channel.getLabel()}" opened`);
|
|
103
|
+
onDataChannel(sessionId, channel);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
peers.set(sessionId, pc);
|
|
107
|
+
return pc;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Dispatch an incoming WebRTC signal from the browser.
|
|
112
|
+
*
|
|
113
|
+
* - `offer`: creates (or reuses) the peer connection and sets the remote
|
|
114
|
+
* description; the library fires `onLocalDescription` automatically with
|
|
115
|
+
* the SDP answer.
|
|
116
|
+
* - `candidate`: adds the ICE candidate to the existing peer connection.
|
|
117
|
+
*
|
|
118
|
+
* @param {string} sessionId
|
|
119
|
+
* @param {WebRtcSignal} signal
|
|
120
|
+
* @returns {void}
|
|
121
|
+
*/
|
|
122
|
+
function handleSignal(sessionId, signal) {
|
|
123
|
+
if (!signal || typeof signal.type !== "string") {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (signal.type === "offer") {
|
|
128
|
+
if (typeof signal.sdp !== "string") {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
log(`[webrtc] Session ${sessionId.slice(0, 8)}: received offer`);
|
|
132
|
+
const pc = getOrCreatePeer(sessionId);
|
|
133
|
+
pc.setRemoteDescription(signal.sdp, "offer");
|
|
134
|
+
// `onLocalDescription` fires automatically with the SDP answer.
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (signal.type === "candidate") {
|
|
139
|
+
const pc = peers.get(sessionId);
|
|
140
|
+
if (!pc) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (typeof signal.candidate === "string" && typeof signal.mid === "string") {
|
|
144
|
+
pc.addRemoteCandidate(signal.candidate, signal.mid);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Close and remove the peer connection for a session.
|
|
151
|
+
* Called automatically when the connection enters a terminal state.
|
|
152
|
+
*
|
|
153
|
+
* @param {string} sessionId
|
|
154
|
+
* @returns {void}
|
|
155
|
+
*/
|
|
156
|
+
function closeSession(sessionId) {
|
|
157
|
+
const pc = peers.get(sessionId);
|
|
158
|
+
if (pc) {
|
|
159
|
+
try { pc.close(); } catch { /* ignore */ }
|
|
160
|
+
peers.delete(sessionId);
|
|
161
|
+
log(`[webrtc] Session ${sessionId.slice(0, 8)}: closed`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return { handleSignal, closeSession };
|
|
166
|
+
}
|