action-engine-js 1.0.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/LICENSE +45 -0
- package/README.md +348 -0
- package/actionengine/3rdparty/goblin/goblin.js +9609 -0
- package/actionengine/3rdparty/goblin/goblin.min.js +5 -0
- package/actionengine/camera/actioncamera.js +90 -0
- package/actionengine/camera/cameracollisionhandler.js +69 -0
- package/actionengine/character/actioncharacter.js +360 -0
- package/actionengine/character/actioncharacter3D.js +61 -0
- package/actionengine/core/app.js +430 -0
- package/actionengine/debug/basedebugpanel.js +858 -0
- package/actionengine/display/canvasmanager.js +75 -0
- package/actionengine/display/gl/programmanager.js +570 -0
- package/actionengine/display/gl/shaders/lineshader.js +118 -0
- package/actionengine/display/gl/shaders/objectshader.js +1756 -0
- package/actionengine/display/gl/shaders/particleshader.js +43 -0
- package/actionengine/display/gl/shaders/shadowshader.js +319 -0
- package/actionengine/display/gl/shaders/spriteshader.js +100 -0
- package/actionengine/display/gl/shaders/watershader.js +67 -0
- package/actionengine/display/graphics/actionmodel3D.js +191 -0
- package/actionengine/display/graphics/actionsprite3D.js +230 -0
- package/actionengine/display/graphics/lighting/actiondirectionalshadowlight.js +864 -0
- package/actionengine/display/graphics/lighting/actionlight.js +211 -0
- package/actionengine/display/graphics/lighting/actionomnidirectionalshadowlight.js +862 -0
- package/actionengine/display/graphics/lighting/lightingconstants.js +263 -0
- package/actionengine/display/graphics/lighting/lightmanager.js +789 -0
- package/actionengine/display/graphics/renderableobject.js +44 -0
- package/actionengine/display/graphics/renderers/actionrenderer2D.js +341 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/actionrenderer3D.js +655 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/canvasmanager3D.js +82 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/debugrenderer3D.js +493 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/objectrenderer3D.js +790 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/spriteRenderer3D.js +266 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/sunrenderer3D.js +140 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/waterrenderer3D.js +173 -0
- package/actionengine/display/graphics/renderers/actionrenderer3D/weatherrenderer3D.js +87 -0
- package/actionengine/display/graphics/texture/proceduraltexture.js +192 -0
- package/actionengine/display/graphics/texture/texturemanager.js +242 -0
- package/actionengine/display/graphics/texture/textureregistry.js +177 -0
- package/actionengine/input/actionscrollablearea.js +1405 -0
- package/actionengine/input/inputhandler.js +1647 -0
- package/actionengine/math/geometry/geometrybuilder.js +161 -0
- package/actionengine/math/geometry/glbexporter.js +364 -0
- package/actionengine/math/geometry/glbloader.js +722 -0
- package/actionengine/math/geometry/modelcodegenerator.js +97 -0
- package/actionengine/math/geometry/triangle.js +33 -0
- package/actionengine/math/geometry/triangleutils.js +34 -0
- package/actionengine/math/mathutils.js +25 -0
- package/actionengine/math/matrix4.js +785 -0
- package/actionengine/math/physics/actionphysics.js +108 -0
- package/actionengine/math/physics/actionphysicsobject3D.js +164 -0
- package/actionengine/math/physics/actionphysicsworld3D.js +238 -0
- package/actionengine/math/physics/actionraycast.js +129 -0
- package/actionengine/math/physics/shapes/actionphysicsbox3D.js +158 -0
- package/actionengine/math/physics/shapes/actionphysicscapsule3D.js +200 -0
- package/actionengine/math/physics/shapes/actionphysicscompoundshape3D.js +147 -0
- package/actionengine/math/physics/shapes/actionphysicscone3D.js +126 -0
- package/actionengine/math/physics/shapes/actionphysicsconvexshape3D.js +72 -0
- package/actionengine/math/physics/shapes/actionphysicscylinder3D.js +117 -0
- package/actionengine/math/physics/shapes/actionphysicsmesh3D.js +74 -0
- package/actionengine/math/physics/shapes/actionphysicsplane3D.js +100 -0
- package/actionengine/math/physics/shapes/actionphysicssphere3D.js +95 -0
- package/actionengine/math/quaternion.js +61 -0
- package/actionengine/math/vector2.js +277 -0
- package/actionengine/math/vector3.js +318 -0
- package/actionengine/math/viewfrustum.js +136 -0
- package/actionengine/network/ACTIONNETREADME.md +810 -0
- package/actionengine/network/client/ActionNetManager.js +802 -0
- package/actionengine/network/client/ActionNetManagerGUI.js +1709 -0
- package/actionengine/network/client/ActionNetManagerP2P.js +1537 -0
- package/actionengine/network/client/SyncSystem.js +422 -0
- package/actionengine/network/p2p/ActionNetPeer.js +142 -0
- package/actionengine/network/p2p/ActionNetTrackerClient.js +623 -0
- package/actionengine/network/p2p/DataConnection.js +282 -0
- package/actionengine/network/p2p/README.md +510 -0
- package/actionengine/network/p2p/example.html +502 -0
- package/actionengine/network/server/ActionNetServer.js +577 -0
- package/actionengine/network/server/ActionNetServerSSL.js +579 -0
- package/actionengine/network/server/ActionNetServerUtils.js +458 -0
- package/actionengine/network/server/SERVERREADME.md +314 -0
- package/actionengine/network/server/package-lock.json +35 -0
- package/actionengine/network/server/package.json +13 -0
- package/actionengine/network/server/start.bat +27 -0
- package/actionengine/network/server/start.sh +25 -0
- package/actionengine/network/server/startwss.bat +27 -0
- package/actionengine/sound/audiomanager.js +1589 -0
- package/actionengine/sound/soundfont/ACTIONSOUNDFONT_README.md +205 -0
- package/actionengine/sound/soundfont/actionparser.js +718 -0
- package/actionengine/sound/soundfont/actionreverb.js +252 -0
- package/actionengine/sound/soundfont/actionsoundfont.js +543 -0
- package/actionengine/sound/soundfont/sf2playerlicence.txt +29 -0
- package/actionengine/sound/soundfont/soundfont.js +2 -0
- package/dist/action-engine.min.js +328 -0
- package/package.json +35 -0
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ActionNetTrackerClient - WebSocket Tracker for Peer Discovery
|
|
3
|
+
*
|
|
4
|
+
* Implements WebSocket tracker protocol for peer discovery via WebRTC offers/answers.
|
|
5
|
+
* Periodically announces with offers to discover peers, manages peer lifecycle.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
class ActionNetTrackerClient {
|
|
9
|
+
constructor(trackerUrls, infohash, peerId, options = {}) {
|
|
10
|
+
// Accept either a single URL or array of URLs
|
|
11
|
+
this.trackerUrls = Array.isArray(trackerUrls) ? trackerUrls : [trackerUrls];
|
|
12
|
+
this.infohash = infohash;
|
|
13
|
+
this.peerId = peerId;
|
|
14
|
+
this.options = {
|
|
15
|
+
debug: options.debug || false,
|
|
16
|
+
numwant: options.numwant || 50,
|
|
17
|
+
announceInterval: options.announceInterval || 5000,
|
|
18
|
+
maxAnnounceInterval: options.maxAnnounceInterval || 120000,
|
|
19
|
+
backoffMultiplier: options.backoffMultiplier || 1.1,
|
|
20
|
+
...options
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
this.trackers = new Map(); // trackerUrl -> { ws, announceInterval, currentInterval, announceCount }
|
|
24
|
+
this.discoveredPeerIds = new Set();
|
|
25
|
+
this.handlers = new Map();
|
|
26
|
+
this.messageCount = 0;
|
|
27
|
+
this.outgoingPeers = new Map(); // Track SimplePeer instances we created: offerId -> peer
|
|
28
|
+
this.pendingOffers = new Set(); // offerId of offers waiting for answers
|
|
29
|
+
this.connectedPeerIds = new Set(); // peerId of peers we've successfully connected to
|
|
30
|
+
this.discoveredCount = 0; // Total peers from tracker (seeders + leechers)
|
|
31
|
+
this.announceCount = 0; // Track total announces for backoff
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Connect to all trackers and start announcing
|
|
36
|
+
* Continues even if some trackers fail to connect
|
|
37
|
+
*/
|
|
38
|
+
async connect() {
|
|
39
|
+
const results = await Promise.allSettled(
|
|
40
|
+
this.trackerUrls.map(url => this._connectToTracker(url))
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// Log any failed trackers but don't throw
|
|
44
|
+
results.forEach((result, index) => {
|
|
45
|
+
if (result.status === 'rejected') {
|
|
46
|
+
this.log(`Tracker connection failed: ${this.trackerUrls[index]} - ${result.reason.message}`, 'warn');
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Check if at least one tracker connected
|
|
51
|
+
if (this.trackers.size === 0) {
|
|
52
|
+
throw new Error('Failed to connect to any trackers');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Connect to a single tracker
|
|
58
|
+
*/
|
|
59
|
+
async _connectToTracker(trackerUrl) {
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
try {
|
|
62
|
+
const ws = new WebSocket(trackerUrl);
|
|
63
|
+
ws.binaryType = 'arraybuffer';
|
|
64
|
+
|
|
65
|
+
ws.onopen = async () => {
|
|
66
|
+
this.log(`Connected to tracker: ${trackerUrl}`);
|
|
67
|
+
try {
|
|
68
|
+
// Wait for first peer to be ready before announcing
|
|
69
|
+
await this._prepareFirstPeer();
|
|
70
|
+
this.log('First peer ready, starting announces');
|
|
71
|
+
this.startAnnouncing();
|
|
72
|
+
this.emit('ready');
|
|
73
|
+
} catch (error) {
|
|
74
|
+
this.log(`Error preparing first peer: ${error.message}`, 'error');
|
|
75
|
+
reject(error);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
resolve();
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
ws.onmessage = (evt) => {
|
|
82
|
+
this.messageCount++;
|
|
83
|
+
this.handleTrackerMessage(evt.data);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
ws.onerror = (error) => {
|
|
87
|
+
const errorMsg = error?.message || String(error);
|
|
88
|
+
this.log(`Tracker error from ${trackerUrl}: ${errorMsg}`, 'error');
|
|
89
|
+
this.emit('error', new Error(errorMsg));
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
ws.onclose = () => {
|
|
93
|
+
this.log(`Disconnected from tracker: ${trackerUrl}`);
|
|
94
|
+
this.trackers.delete(trackerUrl);
|
|
95
|
+
if (this.trackers.size === 0) {
|
|
96
|
+
this.emit('close');
|
|
97
|
+
this.stop();
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Store tracker connection
|
|
102
|
+
this.trackers.set(trackerUrl, { ws, announceInterval: null, currentInterval: this.options.announceInterval, announceCount: 0 });
|
|
103
|
+
|
|
104
|
+
// Timeout for connection
|
|
105
|
+
setTimeout(() => {
|
|
106
|
+
if (ws.readyState !== WebSocket.OPEN) {
|
|
107
|
+
reject(new Error(`Tracker connection timeout: ${trackerUrl}`));
|
|
108
|
+
}
|
|
109
|
+
}, 5000);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
this.log(`Failed to connect to ${trackerUrl}: ${error.message}`, 'error');
|
|
112
|
+
reject(error);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Prepare first peer and wait for ICE to be ready
|
|
119
|
+
*/
|
|
120
|
+
async _prepareFirstPeer() {
|
|
121
|
+
return new Promise((resolve, reject) => {
|
|
122
|
+
const offerId = this.generateOfferId();
|
|
123
|
+
const peer = new ActionNetPeer({
|
|
124
|
+
initiator: true,
|
|
125
|
+
trickle: false,
|
|
126
|
+
iceServers: this.options.iceServers || [
|
|
127
|
+
{ urls: 'stun:stun.l.google.com:19302' }
|
|
128
|
+
]
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
let resolved = false;
|
|
132
|
+
|
|
133
|
+
peer.on('signal', (data) => {
|
|
134
|
+
if (!resolved && data.type === 'offer') {
|
|
135
|
+
resolved = true;
|
|
136
|
+
// Store for later use
|
|
137
|
+
this.outgoingPeers.set(offerId, peer);
|
|
138
|
+
|
|
139
|
+
// Set timeout for this peer
|
|
140
|
+
const timeout = setTimeout(() => {
|
|
141
|
+
if (this.outgoingPeers.has(offerId)) {
|
|
142
|
+
this.outgoingPeers.delete(offerId);
|
|
143
|
+
if (!peer.destroyed) peer.destroy();
|
|
144
|
+
}
|
|
145
|
+
}, 50 * 1000);
|
|
146
|
+
|
|
147
|
+
peer._offerTimeout = timeout;
|
|
148
|
+
resolve();
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
peer.on('error', (err) => {
|
|
153
|
+
if (!resolved) {
|
|
154
|
+
resolved = true;
|
|
155
|
+
if (!peer.destroyed) peer.destroy();
|
|
156
|
+
reject(err);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Timeout if peer takes too long
|
|
161
|
+
setTimeout(() => {
|
|
162
|
+
if (!resolved) {
|
|
163
|
+
resolved = true;
|
|
164
|
+
if (!peer.destroyed) peer.destroy();
|
|
165
|
+
reject(new Error('First peer ICE gathering timeout'));
|
|
166
|
+
}
|
|
167
|
+
}, 10000);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Generate SDP offer using custom Peer class and store the peer
|
|
173
|
+
* Returns { offer, offerId, peer }
|
|
174
|
+
*/
|
|
175
|
+
async generateOffer(offerId) {
|
|
176
|
+
return new Promise((resolve) => {
|
|
177
|
+
const peer = new ActionNetPeer({
|
|
178
|
+
initiator: true,
|
|
179
|
+
trickle: false,
|
|
180
|
+
localPeerId: this.peerId,
|
|
181
|
+
remotePeerId: 'tracker',
|
|
182
|
+
iceServers: this.options.iceServers || [
|
|
183
|
+
{ urls: 'stun:stun.l.google.com:19302' }
|
|
184
|
+
]
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
let offerGenerated = false;
|
|
188
|
+
|
|
189
|
+
peer.on('signal', (data) => {
|
|
190
|
+
if (!offerGenerated && data.type === 'offer') {
|
|
191
|
+
offerGenerated = true;
|
|
192
|
+
// Store the peer for later use when answer arrives
|
|
193
|
+
this.outgoingPeers.set(offerId, peer);
|
|
194
|
+
this.pendingOffers.add(offerId); // Mark as pending
|
|
195
|
+
|
|
196
|
+
// Set a timeout to clean up if answer never comes (2 announce cycles)
|
|
197
|
+
const timeout = setTimeout(() => {
|
|
198
|
+
if (this.outgoingPeers.has(offerId)) {
|
|
199
|
+
this.outgoingPeers.delete(offerId);
|
|
200
|
+
this.pendingOffers.delete(offerId);
|
|
201
|
+
if (!peer.destroyed) peer.destroy();
|
|
202
|
+
}
|
|
203
|
+
}, this.options.announceInterval * 2);
|
|
204
|
+
|
|
205
|
+
// Attach timeout to peer so we can clear it later
|
|
206
|
+
peer._offerTimeout = timeout;
|
|
207
|
+
|
|
208
|
+
resolve({ offer: data, offerId, peer });
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
peer.on('error', (err) => {
|
|
213
|
+
console.warn('Error generating offer:', err);
|
|
214
|
+
if (!peer.destroyed) peer.destroy();
|
|
215
|
+
if (this.outgoingPeers.has(offerId)) {
|
|
216
|
+
this.outgoingPeers.delete(offerId);
|
|
217
|
+
}
|
|
218
|
+
resolve(null);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Start periodic announcements with offers
|
|
225
|
+
*/
|
|
226
|
+
startAnnouncing() {
|
|
227
|
+
// Announce immediately with offers (first peer is ready)
|
|
228
|
+
this.announceWithOffers();
|
|
229
|
+
|
|
230
|
+
// Then announce with offers at specified interval with backoff to all trackers
|
|
231
|
+
for (const [trackerUrl, trackerData] of this.trackers) {
|
|
232
|
+
if (trackerData.announceInterval) clearInterval(trackerData.announceInterval);
|
|
233
|
+
|
|
234
|
+
const scheduleNextAnnounce = () => {
|
|
235
|
+
trackerData.announceInterval = setTimeout(() => {
|
|
236
|
+
if (trackerData.ws && trackerData.ws.readyState === WebSocket.OPEN) {
|
|
237
|
+
trackerData.announceCount++;
|
|
238
|
+
this.announceWithOffers(trackerData.ws);
|
|
239
|
+
|
|
240
|
+
// Increase interval by backoff multiplier, cap at maxAnnounceInterval
|
|
241
|
+
trackerData.currentInterval = Math.min(
|
|
242
|
+
trackerData.currentInterval * this.options.backoffMultiplier,
|
|
243
|
+
this.options.maxAnnounceInterval
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
// Schedule next announce with new interval
|
|
247
|
+
scheduleNextAnnounce();
|
|
248
|
+
}
|
|
249
|
+
}, trackerData.currentInterval);
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
scheduleNextAnnounce();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Generate offers and announce to all trackers
|
|
258
|
+
*/
|
|
259
|
+
async announceWithOffers(targetWs = null) {
|
|
260
|
+
try {
|
|
261
|
+
// Don't generate new offers if we have pending unanswered ones
|
|
262
|
+
if (this.pendingOffers.size > 0) {
|
|
263
|
+
this.announce([], targetWs); // Just announce without new offers
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Generate 1 offer
|
|
268
|
+
const offerId = this.generateOfferId();
|
|
269
|
+
const result = await this.generateOffer(offerId);
|
|
270
|
+
|
|
271
|
+
const offers = result && result.offer ? [{
|
|
272
|
+
offer_id: offerId,
|
|
273
|
+
offer: result.offer
|
|
274
|
+
}] : [];
|
|
275
|
+
|
|
276
|
+
this.announce(offers, targetWs);
|
|
277
|
+
} catch (error) {
|
|
278
|
+
this.log(`Error generating offer: ${error.message}`, 'error');
|
|
279
|
+
this.announce([], targetWs); // Announce without offers
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Generate random offer ID (20 bytes hex)
|
|
285
|
+
*/
|
|
286
|
+
generateOfferId() {
|
|
287
|
+
const bytes = new Uint8Array(20);
|
|
288
|
+
crypto.getRandomValues(bytes);
|
|
289
|
+
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Send announce request to tracker(s) with offers
|
|
294
|
+
*/
|
|
295
|
+
announce(offers = [], targetWs = null) {
|
|
296
|
+
// Build announce message
|
|
297
|
+
const request = {
|
|
298
|
+
action: 'announce',
|
|
299
|
+
info_hash: this.infohash,
|
|
300
|
+
peer_id: this.peerId,
|
|
301
|
+
numwant: this.options.numwant
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
// Add offers if we have them
|
|
305
|
+
if (offers.length > 0) {
|
|
306
|
+
request.offers = offers;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const msgStr = JSON.stringify(request);
|
|
310
|
+
console.log('[TrackerClient] Announcing:', msgStr);
|
|
311
|
+
this.log(`Announcing with ${offers.length} offers`);
|
|
312
|
+
|
|
313
|
+
// Send to specific tracker or all
|
|
314
|
+
if (targetWs) {
|
|
315
|
+
if (targetWs.readyState === WebSocket.OPEN) {
|
|
316
|
+
targetWs.send(msgStr);
|
|
317
|
+
}
|
|
318
|
+
} else {
|
|
319
|
+
// Announce to all connected trackers
|
|
320
|
+
for (const [, trackerData] of this.trackers) {
|
|
321
|
+
if (trackerData.ws && trackerData.ws.readyState === WebSocket.OPEN) {
|
|
322
|
+
trackerData.ws.send(msgStr);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Handle tracker response (JSON format)
|
|
330
|
+
*/
|
|
331
|
+
handleTrackerMessage(data) {
|
|
332
|
+
try {
|
|
333
|
+
let message;
|
|
334
|
+
|
|
335
|
+
if (data instanceof ArrayBuffer) {
|
|
336
|
+
const str = new TextDecoder().decode(data);
|
|
337
|
+
message = JSON.parse(str);
|
|
338
|
+
} else {
|
|
339
|
+
const str = typeof data === 'string' ? data : new TextDecoder().decode(data);
|
|
340
|
+
message = JSON.parse(str);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
console.log(`[TrackerClient] MESSAGE #${this.messageCount}:`, message);
|
|
344
|
+
|
|
345
|
+
// Check for peer offer/answer FIRST before general announce response
|
|
346
|
+
// (tracker sends all of these with action: 'announce')
|
|
347
|
+
if (message.offer && message.peer_id) {
|
|
348
|
+
// Tracker is relaying a peer's offer
|
|
349
|
+
this.handlePeerOffer(message);
|
|
350
|
+
} else if (message.answer && message.peer_id) {
|
|
351
|
+
// Peer is answering our offer
|
|
352
|
+
this.handlePeerAnswer(message);
|
|
353
|
+
} else if (message['failure reason']) {
|
|
354
|
+
// Tracker sent a failure response
|
|
355
|
+
this.log(`Tracker failure: ${message['failure reason']}`, 'warn');
|
|
356
|
+
console.log('[TrackerClient] Tracker failure:', message['failure reason']);
|
|
357
|
+
} else if (message.action === 'announce') {
|
|
358
|
+
// Regular announce response (with stats)
|
|
359
|
+
this.handleAnnounceResponse(message);
|
|
360
|
+
} else if (message.action === 'scrape') {
|
|
361
|
+
// Scrape response
|
|
362
|
+
this.emit('scrape', message);
|
|
363
|
+
} else {
|
|
364
|
+
console.log('[TrackerClient] Unknown message type:', Object.keys(message).join(', '));
|
|
365
|
+
}
|
|
366
|
+
} catch (error) {
|
|
367
|
+
this.log(`Failed to parse tracker message: ${error.message}`, 'error');
|
|
368
|
+
console.error('[TrackerClient] Raw data:', data);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Handle announce response (ACK from tracker with stats)
|
|
374
|
+
*/
|
|
375
|
+
handleAnnounceResponse(response) {
|
|
376
|
+
console.log('[TrackerClient] Announce response:', response);
|
|
377
|
+
|
|
378
|
+
const complete = response.complete || 0;
|
|
379
|
+
const incomplete = response.incomplete || 0;
|
|
380
|
+
this.discoveredCount = complete + incomplete;
|
|
381
|
+
|
|
382
|
+
this.log(`Tracker stats: ${complete} seeders, ${incomplete} leechers (${this.discoveredCount} total)`);
|
|
383
|
+
|
|
384
|
+
this.emit('update', {
|
|
385
|
+
complete: complete,
|
|
386
|
+
incomplete: incomplete
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Handle offer from another peer (tracker is relaying their offer)
|
|
392
|
+
*/
|
|
393
|
+
handlePeerOffer(message) {
|
|
394
|
+
const peerId = message.peer_id;
|
|
395
|
+
const offerId = message.offer_id;
|
|
396
|
+
const offer = message.offer;
|
|
397
|
+
|
|
398
|
+
console.log('[TrackerClient] Received offer from peer:', peerId);
|
|
399
|
+
|
|
400
|
+
// Skip if we already have a connection to this peer
|
|
401
|
+
if (this.connectedPeerIds.has(peerId)) {
|
|
402
|
+
console.log('[TrackerClient] Already connected to peer:', peerId, '- ignoring offer');
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Create responder Peer to handle this offer
|
|
407
|
+
const peer = new ActionNetPeer({
|
|
408
|
+
initiator: false,
|
|
409
|
+
trickle: false,
|
|
410
|
+
localPeerId: this.peerId,
|
|
411
|
+
remotePeerId: peerId,
|
|
412
|
+
iceServers: this.options.iceServers || [
|
|
413
|
+
{ urls: 'stun:stun.l.google.com:19302' }
|
|
414
|
+
]
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
// Store peer so we can send answer back
|
|
418
|
+
this.outgoingPeers.set(offerId, {
|
|
419
|
+
peer: peer,
|
|
420
|
+
peerId: peerId,
|
|
421
|
+
offerId: offerId
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
// Listen for answer signal
|
|
425
|
+
peer.on('signal', (data) => {
|
|
426
|
+
if (data.type === 'answer') {
|
|
427
|
+
console.log('[TrackerClient] Sending answer to peer:', peerId);
|
|
428
|
+
this.sendAnswer(offerId, peerId, data);
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// Emit peer's internal DataConnection when tracker signaling is complete
|
|
433
|
+
peer.once('connect', () => {
|
|
434
|
+
this.connectedPeerIds.add(peerId);
|
|
435
|
+
|
|
436
|
+
// Emit peer event
|
|
437
|
+
this.emit('peer', {
|
|
438
|
+
id: peerId,
|
|
439
|
+
peer: peer,
|
|
440
|
+
source: 'tracker'
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
// ActionNetPeer has an internal DataConnection
|
|
444
|
+
// Emit that connection directly (no extra wrapper needed)
|
|
445
|
+
this.emit('connection', peer.connection);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
peer.on('error', (err) => {
|
|
449
|
+
console.warn('[TrackerClient] Responder peer error:', err.message);
|
|
450
|
+
this.outgoingPeers.delete(offerId);
|
|
451
|
+
this.connectedPeerIds.delete(peerId);
|
|
452
|
+
this.emit('peer-failed', {
|
|
453
|
+
id: peerId,
|
|
454
|
+
error: err
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
peer.on('close', () => {
|
|
459
|
+
this.outgoingPeers.delete(offerId);
|
|
460
|
+
this.connectedPeerIds.delete(peerId);
|
|
461
|
+
this.emit('peer-disconnected', {
|
|
462
|
+
id: peerId
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
// Signal the offer
|
|
467
|
+
peer.signal(offer);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Handle answer from peer (responding to our offer)
|
|
472
|
+
*/
|
|
473
|
+
handlePeerAnswer(message) {
|
|
474
|
+
const peerId = message.peer_id;
|
|
475
|
+
const offerId = message.offer_id;
|
|
476
|
+
const answer = message.answer;
|
|
477
|
+
|
|
478
|
+
console.log('[TrackerClient] Received answer from peer:', peerId);
|
|
479
|
+
|
|
480
|
+
// Check if we have the SimplePeer for this offer
|
|
481
|
+
const peer = this.outgoingPeers.get(offerId);
|
|
482
|
+
if (peer && !peer.destroyed) {
|
|
483
|
+
// Mark offer as answered (no longer pending)
|
|
484
|
+
this.pendingOffers.delete(offerId);
|
|
485
|
+
|
|
486
|
+
// Now we know the real remote peer ID, update the connection
|
|
487
|
+
if (peer.connection) {
|
|
488
|
+
peer.connection.remotePeerId = peerId;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Signal the answer to the peer
|
|
492
|
+
peer.signal(answer);
|
|
493
|
+
|
|
494
|
+
// Clear the timeout since we got a response
|
|
495
|
+
if (peer._offerTimeout) {
|
|
496
|
+
clearTimeout(peer._offerTimeout);
|
|
497
|
+
peer._offerTimeout = null;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Emit peer's internal DataConnection when tracker signaling is complete
|
|
501
|
+
peer.once('connect', () => {
|
|
502
|
+
// Mark this peer ID as connected
|
|
503
|
+
this.connectedPeerIds.add(peerId);
|
|
504
|
+
|
|
505
|
+
// Emit peer event
|
|
506
|
+
this.emit('peer', {
|
|
507
|
+
id: peerId,
|
|
508
|
+
peer: peer,
|
|
509
|
+
source: 'tracker'
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
// ActionNetPeer has an internal DataConnection
|
|
513
|
+
// Emit that connection directly (no extra wrapper needed)
|
|
514
|
+
this.emit('connection', peer.connection);
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
peer.on('error', (err) => {
|
|
518
|
+
console.warn('[TrackerClient] Peer error:', err.message);
|
|
519
|
+
this.outgoingPeers.delete(offerId);
|
|
520
|
+
this.connectedPeerIds.delete(peerId);
|
|
521
|
+
this.emit('peer-failed', {
|
|
522
|
+
id: peerId,
|
|
523
|
+
error: err
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
peer.on('close', () => {
|
|
528
|
+
this.outgoingPeers.delete(offerId);
|
|
529
|
+
this.connectedPeerIds.delete(peerId);
|
|
530
|
+
this.emit('peer-disconnected', {
|
|
531
|
+
id: peerId
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
} else {
|
|
535
|
+
console.warn('[TrackerClient] Got answer for unknown offer:', offerId);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Send answer back to tracker(s) (responding to a peer's offer)
|
|
541
|
+
*/
|
|
542
|
+
sendAnswer(offerId, peerId, answer) {
|
|
543
|
+
const response = {
|
|
544
|
+
action: 'announce',
|
|
545
|
+
info_hash: this.infohash,
|
|
546
|
+
peer_id: this.peerId,
|
|
547
|
+
to_peer_id: peerId,
|
|
548
|
+
offer_id: offerId,
|
|
549
|
+
answer: answer
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
const msgStr = JSON.stringify(response);
|
|
553
|
+
console.log('[TrackerClient] Sending answer:', msgStr);
|
|
554
|
+
|
|
555
|
+
// Send to all connected trackers
|
|
556
|
+
for (const [, trackerData] of this.trackers) {
|
|
557
|
+
if (trackerData.ws && trackerData.ws.readyState === WebSocket.OPEN) {
|
|
558
|
+
trackerData.ws.send(msgStr);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Stop announcing to all trackers
|
|
565
|
+
*/
|
|
566
|
+
stop() {
|
|
567
|
+
for (const [, trackerData] of this.trackers) {
|
|
568
|
+
if (trackerData.announceInterval) {
|
|
569
|
+
clearInterval(trackerData.announceInterval);
|
|
570
|
+
trackerData.announceInterval = null;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Disconnect from all trackers
|
|
577
|
+
*/
|
|
578
|
+
disconnect() {
|
|
579
|
+
this.stop();
|
|
580
|
+
for (const [, trackerData] of this.trackers) {
|
|
581
|
+
if (trackerData.ws) {
|
|
582
|
+
trackerData.ws.close();
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
this.trackers.clear();
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Get discovered peer count (seeders + leechers from tracker)
|
|
590
|
+
*/
|
|
591
|
+
getDiscoveredPeerCount() {
|
|
592
|
+
return this.discoveredCount;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Event handlers
|
|
597
|
+
*/
|
|
598
|
+
on(event, handler) {
|
|
599
|
+
if (!this.handlers.has(event)) {
|
|
600
|
+
this.handlers.set(event, []);
|
|
601
|
+
}
|
|
602
|
+
this.handlers.get(event).push(handler);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
emit(event, ...args) {
|
|
606
|
+
if (!this.handlers.has(event)) return;
|
|
607
|
+
for (const handler of this.handlers.get(event)) {
|
|
608
|
+
try {
|
|
609
|
+
handler(...args);
|
|
610
|
+
} catch (e) {
|
|
611
|
+
this.log(`Error in ${event} handler: ${e.message}`, 'error');
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Logging
|
|
618
|
+
*/
|
|
619
|
+
log(msg, level = 'info') {
|
|
620
|
+
if (!this.options.debug) return;
|
|
621
|
+
console.log(`[TrackerClient] ${msg}`);
|
|
622
|
+
}
|
|
623
|
+
}
|