p2p-media-loader-core
Version:
P2P Media Loader core functionality
633 lines • 39.7 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
if (kind === "m") throw new TypeError("Private method is not writable");
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var _WebTorrentClient_instances, _a, _WebTorrentClient_DEFAULT_ANNOUNCE_INTERVAL_SECONDS, _WebTorrentClient_MIN_ANNOUNCE_INTERVAL_SECONDS, _WebTorrentClient_config, _WebTorrentClient_wsClient, _WebTorrentClient_eventTarget, _WebTorrentClient_pendingOffers, _WebTorrentClient_negotiatingConnections, _WebTorrentClient_destroyAbortController, _WebTorrentClient_announceTimeoutId, _WebTorrentClient_announceIntervalSeconds, _WebTorrentClient_scheduleAnnounceRunId, _WebTorrentClient_activeAnnouncePromise, _WebTorrentClient_nextAnnounceEvent, _WebTorrentClient_trackerId, _WebTorrentClient_started, _WebTorrentClient_isDestroyed, _WebTorrentClient_throwIfDestroyed, _WebTorrentClient_onWsConnected, _WebTorrentClient_onWsDisconnected, _WebTorrentClient_onWsMessage, _WebTorrentClient_scheduleAnnounce, _WebTorrentClient_clearAnnounceTimeout, _WebTorrentClient_announce, _WebTorrentClient_createOffer, _WebTorrentClient_sendStopped, _WebTorrentClient_buildAnnouncePayload, _WebTorrentClient_handleIncomingOffer, _WebTorrentClient_handleIncomingAnswer, _WebTorrentClient_waitForIceGathering, _WebTorrentClient_waitForConnection, _WebTorrentClient_cleanupPendingOffer, _WebTorrentClient_cleanupPendingOffers, _WebTorrentClient_cleanupNegotiatingConnections;
import { EventTarget } from "../../utils/event-target.js";
import { getPromiseWithResolvers } from "../../utils/utils.js";
import { isTerminalConnectionState } from "../utils.js";
import { TrackerError, TrackerWarning, PeerConnectError } from "../../types.js";
import { SafeAbortController } from "../../utils/abort-controller.js";
import { PeerConnection, SessionDescription, safeCreateOffer, safeCreateAnswer, safeSetLocalDescription, safeSetRemoteDescription, } from "./webrtc-utils.js";
const WEBTORRENT_DEFAULT_OFFER_TIMEOUT = 50000;
const WEBTORRENT_DEFAULT_CONNECTION_TIMEOUT = 15000;
const WEBTORRENT_DEFAULT_OFFERS_COUNT = 5;
const WEBTORRENT_DEFAULT_ICE_GATHERING_TIMEOUT = 5000;
function generateOfferId() {
// Generate a safe 20-character alphanumeric string
let id = "";
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (let i = 0; i < 20; i++) {
id += chars.charAt(Math.floor(Math.random() * chars.length));
}
return id;
}
const VALID_SDP_TYPES = new Set(["offer", "answer", "pranswer", "rollback"]);
function isSessionDescriptionInit(value) {
if (typeof value !== "object" || value === null)
return false;
const obj = value;
return (typeof obj.type === "string" &&
VALID_SDP_TYPES.has(obj.type) &&
typeof obj.sdp === "string");
}
export class WebTorrentClient {
constructor(config) {
var _b, _c, _d, _e, _f, _g;
_WebTorrentClient_instances.add(this);
_WebTorrentClient_config.set(this, void 0);
_WebTorrentClient_wsClient.set(this, void 0);
_WebTorrentClient_eventTarget.set(this, new EventTarget());
_WebTorrentClient_pendingOffers.set(this, new Map());
_WebTorrentClient_negotiatingConnections.set(this, new Set());
_WebTorrentClient_destroyAbortController.set(this, new SafeAbortController());
_WebTorrentClient_announceTimeoutId.set(this, null);
_WebTorrentClient_announceIntervalSeconds.set(this, null);
_WebTorrentClient_scheduleAnnounceRunId.set(this, 0);
_WebTorrentClient_activeAnnouncePromise.set(this, null);
_WebTorrentClient_nextAnnounceEvent.set(this, undefined);
_WebTorrentClient_trackerId.set(this, null);
_WebTorrentClient_started.set(this, false);
_WebTorrentClient_onWsConnected.set(this, () => {
// Setup a fallback interval in case the tracker doesn't provide one
__classPrivateFieldGet(this, _WebTorrentClient_instances, "m", _WebTorrentClient_scheduleAnnounce).call(this, __classPrivateFieldGet(_a, _a, "f", _WebTorrentClient_DEFAULT_ANNOUNCE_INTERVAL_SECONDS));
// Send the initial announce event
__classPrivateFieldGet(this, _WebTorrentClient_instances, "m", _WebTorrentClient_announce).call(this, "started").catch((err) => {
if (__classPrivateFieldGet(this, _WebTorrentClient_instances, "m", _WebTorrentClient_isDestroyed).call(this))
return;
__classPrivateFieldGet(this, _WebTorrentClient_eventTarget, "f").dispatchEvent("error", new TrackerError("announce-failed", `Initial announce failed: ${err instanceof Error ? err.message : String(err)}`, err));
});
});
_WebTorrentClient_onWsDisconnected.set(this, () => {
__classPrivateFieldGet(this, _WebTorrentClient_instances, "m", _WebTorrentClient_clearAnnounceTimeout).call(this);
__classPrivateFieldSet(this, _WebTorrentClient_announceIntervalSeconds, null, "f");
});
_WebTorrentClient_onWsMessage.set(this, (data) => {
if (__classPrivateFieldGet(this, _WebTorrentClient_instances, "m", _WebTorrentClient_isDestroyed).call(this))
return;
let msg;
try {
const text = typeof data === "string" ? data : new TextDecoder().decode(data);
msg = JSON.parse(text);
}
catch (err) {
__classPrivateFieldGet(this, _WebTorrentClient_eventTarget, "f").dispatchEvent("error", new TrackerError("parse-error", `Failed to parse tracker message: ${err instanceof Error ? err.message : String(err)}`, err));
return;
}
if (typeof msg !== "object" || msg === null || Array.isArray(msg))
return;
const dataObject = msg;
// Ignore messages for a different torrent (possible on shared WebSocket connections)
const infoHash = dataObject.info_hash;
if (typeof infoHash === "string" && infoHash !== __classPrivateFieldGet(this, _WebTorrentClient_config, "f").infoHash) {
return;
}
const warningMessage = dataObject["warning message"];
if (typeof warningMessage === "string") {
__classPrivateFieldGet(this, _WebTorrentClient_eventTarget, "f").dispatchEvent("warning", new TrackerWarning("tracker-response", warningMessage));
}
const failureReason = dataObject["failure reason"];
if (typeof failureReason === "string") {
__classPrivateFieldGet(this, _WebTorrentClient_eventTarget, "f").dispatchEvent("error", new TrackerError("tracker-response", failureReason));
return;
}
const { interval } = dataObject;
if (typeof interval === "number" && interval > 0) {
// Defend against trackers asking for extremely frequent announces
const safeInterval = Math.max(__classPrivateFieldGet(_a, _a, "f", _WebTorrentClient_MIN_ANNOUNCE_INTERVAL_SECONDS), interval);
if (__classPrivateFieldGet(this, _WebTorrentClient_announceIntervalSeconds, "f") !== safeInterval) {
__classPrivateFieldGet(this, _WebTorrentClient_instances, "m", _WebTorrentClient_scheduleAnnounce).call(this, safeInterval);
}
}
// The WebTorrent tracker protocol specifies "tracker id" (with a space) in the
// response, but expects it to be echoed back as "trackerid" (no space) in
// subsequent announce requests. We store it here and send it back later.
const trackerId = dataObject["tracker id"];
if (typeof trackerId === "string") {
__classPrivateFieldSet(this, _WebTorrentClient_trackerId, trackerId, "f");
}
// Ignore offers/answers from ourselves
const peerId = dataObject.peer_id;
if (typeof peerId === "string" && peerId === __classPrivateFieldGet(this, _WebTorrentClient_config, "f").peerId) {
return;
}
// Both offer and answer messages require peer_id and offer_id
const offerId = dataObject.offer_id;
if (typeof peerId !== "string" || typeof offerId !== "string")
return;
if (isSessionDescriptionInit(dataObject.offer)) {
__classPrivateFieldGet(this, _WebTorrentClient_instances, "m", _WebTorrentClient_handleIncomingOffer).call(this, {
sdp: dataObject.offer,
peerId,
offerId,
}).catch((err) => {
if (__classPrivateFieldGet(this, _WebTorrentClient_instances, "m", _WebTorrentClient_isDestroyed).call(this))
return;
__classPrivateFieldGet(this, _WebTorrentClient_eventTarget, "f").dispatchEvent("error", new TrackerError("signaling-failed", `Failed to handle offer: ${err instanceof Error ? err.message : String(err)}`, err));
});
}
else if (isSessionDescriptionInit(dataObject.answer)) {
__classPrivateFieldGet(this, _WebTorrentClient_instances, "m", _WebTorrentClient_handleIncomingAnswer).call(this, {
sdp: dataObject.answer,
peerId,
offerId,
}).catch((err) => {
if (__classPrivateFieldGet(this, _WebTorrentClient_instances, "m", _WebTorrentClient_isDestroyed).call(this))
return;
__classPrivateFieldGet(this, _WebTorrentClient_eventTarget, "f").dispatchEvent("error", new TrackerError("signaling-failed", `Failed to handle answer: ${err instanceof Error ? err.message : String(err)}`, err));
});
}
});
__classPrivateFieldSet(this, _WebTorrentClient_config, {
infoHash: config.infoHash,
peerId: config.peerId,
rtcConfig: config.rtcConfig,
channelConfig: config.channelConfig,
offerTimeout: (_b = config.offerTimeout) !== null && _b !== void 0 ? _b : (() => WEBTORRENT_DEFAULT_OFFER_TIMEOUT),
offersCount: (_c = config.offersCount) !== null && _c !== void 0 ? _c : (() => WEBTORRENT_DEFAULT_OFFERS_COUNT),
iceGatheringTimeout: (_d = config.iceGatheringTimeout) !== null && _d !== void 0 ? _d : (() => WEBTORRENT_DEFAULT_ICE_GATHERING_TIMEOUT),
connectionTimeout: (_e = config.connectionTimeout) !== null && _e !== void 0 ? _e : (() => WEBTORRENT_DEFAULT_CONNECTION_TIMEOUT),
claimPeer: (_f = config.claimPeer) !== null && _f !== void 0 ? _f : (() => true),
shouldGenerateOffers: (_g = config.shouldGenerateOffers) !== null && _g !== void 0 ? _g : (() => true),
}, "f");
__classPrivateFieldSet(this, _WebTorrentClient_wsClient, config.wsClient, "f");
}
addEventListener(eventName, listener) {
__classPrivateFieldGet(this, _WebTorrentClient_eventTarget, "f").addEventListener(eventName, listener);
}
removeEventListener(eventName, listener) {
__classPrivateFieldGet(this, _WebTorrentClient_eventTarget, "f").removeEventListener(eventName, listener);
}
start() {
if (__classPrivateFieldGet(this, _WebTorrentClient_instances, "m", _WebTorrentClient_isDestroyed).call(this) || __classPrivateFieldGet(this, _WebTorrentClient_started, "f"))
return;
__classPrivateFieldSet(this, _WebTorrentClient_started, true, "f");
__classPrivateFieldGet(this, _WebTorrentClient_wsClient, "f").addEventListener("connected", __classPrivateFieldGet(this, _WebTorrentClient_onWsConnected, "f"));
__classPrivateFieldGet(this, _WebTorrentClient_wsClient, "f").addEventListener("disconnected", __classPrivateFieldGet(this, _WebTorrentClient_onWsDisconnected, "f"));
__classPrivateFieldGet(this, _WebTorrentClient_wsClient, "f").addEventListener("message", __classPrivateFieldGet(this, _WebTorrentClient_onWsMessage, "f"));
if (__classPrivateFieldGet(this, _WebTorrentClient_wsClient, "f").state === "connected") {
__classPrivateFieldGet(this, _WebTorrentClient_onWsConnected, "f").call(this);
}
}
destroy() {
if (__classPrivateFieldGet(this, _WebTorrentClient_instances, "m", _WebTorrentClient_isDestroyed).call(this))
return;
__classPrivateFieldGet(this, _WebTorrentClient_destroyAbortController, "f").abort();
__classPrivateFieldGet(this, _WebTorrentClient_instances, "m", _WebTorrentClient_clearAnnounceTimeout).call(this);
__classPrivateFieldGet(this, _WebTorrentClient_instances, "m", _WebTorrentClient_sendStopped).call(this);
__classPrivateFieldGet(this, _WebTorrentClient_instances, "m", _WebTorrentClient_cleanupPendingOffers).call(this);
__classPrivateFieldGet(this, _WebTorrentClient_instances, "m", _WebTorrentClient_cleanupNegotiatingConnections).call(this);
if (__classPrivateFieldGet(this, _WebTorrentClient_started, "f")) {
__classPrivateFieldGet(this, _WebTorrentClient_wsClient, "f").removeEventListener("connected", __classPrivateFieldGet(this, _WebTorrentClient_onWsConnected, "f"));
__classPrivateFieldGet(this, _WebTorrentClient_wsClient, "f").removeEventListener("disconnected", __classPrivateFieldGet(this, _WebTorrentClient_onWsDisconnected, "f"));
__classPrivateFieldGet(this, _WebTorrentClient_wsClient, "f").removeEventListener("message", __classPrivateFieldGet(this, _WebTorrentClient_onWsMessage, "f"));
}
__classPrivateFieldGet(this, _WebTorrentClient_eventTarget, "f").clear();
}
}
_a = WebTorrentClient, _WebTorrentClient_config = new WeakMap(), _WebTorrentClient_wsClient = new WeakMap(), _WebTorrentClient_eventTarget = new WeakMap(), _WebTorrentClient_pendingOffers = new WeakMap(), _WebTorrentClient_negotiatingConnections = new WeakMap(), _WebTorrentClient_destroyAbortController = new WeakMap(), _WebTorrentClient_announceTimeoutId = new WeakMap(), _WebTorrentClient_announceIntervalSeconds = new WeakMap(), _WebTorrentClient_scheduleAnnounceRunId = new WeakMap(), _WebTorrentClient_activeAnnouncePromise = new WeakMap(), _WebTorrentClient_nextAnnounceEvent = new WeakMap(), _WebTorrentClient_trackerId = new WeakMap(), _WebTorrentClient_started = new WeakMap(), _WebTorrentClient_onWsConnected = new WeakMap(), _WebTorrentClient_onWsDisconnected = new WeakMap(), _WebTorrentClient_onWsMessage = new WeakMap(), _WebTorrentClient_instances = new WeakSet(), _WebTorrentClient_isDestroyed = function _WebTorrentClient_isDestroyed() {
return __classPrivateFieldGet(this, _WebTorrentClient_destroyAbortController, "f").signal.aborted;
}, _WebTorrentClient_throwIfDestroyed = function _WebTorrentClient_throwIfDestroyed() {
if (__classPrivateFieldGet(this, _WebTorrentClient_destroyAbortController, "f").signal.aborted) {
throw new Error("Client destroyed");
}
}, _WebTorrentClient_scheduleAnnounce = function _WebTorrentClient_scheduleAnnounce(intervalSeconds) {
var _b;
__classPrivateFieldGet(this, _WebTorrentClient_instances, "m", _WebTorrentClient_clearAnnounceTimeout).call(this);
__classPrivateFieldSet(this, _WebTorrentClient_announceIntervalSeconds, intervalSeconds, "f");
const runId = __classPrivateFieldSet(this, _WebTorrentClient_scheduleAnnounceRunId, (_b = __classPrivateFieldGet(this, _WebTorrentClient_scheduleAnnounceRunId, "f"), ++_b), "f");
const run = () => __awaiter(this, void 0, void 0, function* () {
try {
yield __classPrivateFieldGet(this, _WebTorrentClient_instances, "m", _WebTorrentClient_announce).call(this);
}
catch (err) {
if (__classPrivateFieldGet(this, _WebTorrentClient_instances, "m", _WebTorrentClient_isDestroyed).call(this))
return;
__classPrivateFieldGet(this, _WebTorrentClient_eventTarget, "f").dispatchEvent("error", new TrackerError("announce-failed", `Announce failed: ${err instanceof Error ? err.message : String(err)}`, err));
}
if (!__classPrivateFieldGet(this, _WebTorrentClient_instances, "m", _WebTorrentClient_isDestroyed).call(this) &&
__classPrivateFieldGet(this, _WebTorrentClient_announceIntervalSeconds, "f") !== null &&
__classPrivateFieldGet(this, _WebTorrentClient_scheduleAnnounceRunId, "f") === runId) {
__classPrivateFieldSet(this, _WebTorrentClient_announceTimeoutId, setTimeout(run, __classPrivateFieldGet(this, _WebTorrentClient_announceIntervalSeconds, "f") * 1000), "f");
}
});
__classPrivateFieldSet(this, _WebTorrentClient_announceTimeoutId, setTimeout(run, intervalSeconds * 1000), "f");
}, _WebTorrentClient_clearAnnounceTimeout = function _WebTorrentClient_clearAnnounceTimeout() {
if (__classPrivateFieldGet(this, _WebTorrentClient_announceTimeoutId, "f") !== null) {
clearTimeout(__classPrivateFieldGet(this, _WebTorrentClient_announceTimeoutId, "f"));
__classPrivateFieldSet(this, _WebTorrentClient_announceTimeoutId, null, "f");
}
}, _WebTorrentClient_announce = function _WebTorrentClient_announce(event) {
return __awaiter(this, void 0, void 0, function* () {
if (__classPrivateFieldGet(this, _WebTorrentClient_instances, "m", _WebTorrentClient_isDestroyed).call(this) || __classPrivateFieldGet(this, _WebTorrentClient_wsClient, "f").state !== "connected")
return;
if (event) {
__classPrivateFieldSet(this, _WebTorrentClient_nextAnnounceEvent, event, "f");
}
if (__classPrivateFieldGet(this, _WebTorrentClient_activeAnnouncePromise, "f")) {
return __classPrivateFieldGet(this, _WebTorrentClient_activeAnnouncePromise, "f");
}
const promise = (() => __awaiter(this, void 0, void 0, function* () {
const shouldGenerateOffers = __classPrivateFieldGet(this, _WebTorrentClient_config, "f").shouldGenerateOffers();
const offersCount = shouldGenerateOffers ? __classPrivateFieldGet(this, _WebTorrentClient_config, "f").offersCount() : 0;
// Generate offers in parallel to avoid sequential ICE gathering latency.
// Each #createOffer() internally catches its own errors and returns
// undefined on failure, so Promise.all() will never reject here.
// We avoid Promise.allSettled() for older browser compatibility.
const results = yield Promise.all(Array.from({ length: offersCount }, () => __classPrivateFieldGet(this, _WebTorrentClient_instances, "m", _WebTorrentClient_createOffer).call(this)));
if (__classPrivateFieldGet(this, _WebTorrentClient_instances, "m", _WebTorrentClient_isDestroyed).call(this)) {
for (const result of results) {
if (result) {
__classPrivateFieldGet(this, _WebTorrentClient_instances, "m", _WebTorrentClient_cleanupPendingOffer).call(this, result.offer_id);
}
}
return;
}
const offers = [];
for (const result of results) {
if (result) {
offers.push(result);
}
}
const currentEvent = __classPrivateFieldGet(this, _WebTorrentClient_nextAnnounceEvent, "f");
__classPrivateFieldSet(this, _WebTorrentClient_nextAnnounceEvent, undefined, "f");
const payload = __classPrivateFieldGet(this, _WebTorrentClient_instances, "m", _WebTorrentClient_buildAnnouncePayload).call(this, {
numwant: offers.length,
offers,
event: currentEvent,
});
if (__classPrivateFieldGet(this, _WebTorrentClient_wsClient, "f").state !== "connected") {
for (const offer of offers) {
__classPrivateFieldGet(this, _WebTorrentClient_instances, "m", _WebTorrentClient_cleanupPendingOffer).call(this, offer.offer_id);
}
return;
}
try {
__classPrivateFieldGet(this, _WebTorrentClient_wsClient, "f").send(JSON.stringify(payload));
}
catch (err) {
for (const offer of offers) {
__classPrivateFieldGet(this, _WebTorrentClient_instances, "m", _WebTorrentClient_cleanupPendingOffer).call(this, offer.offer_id);
}
throw err;
}
}))();
__classPrivateFieldSet(this, _WebTorrentClient_activeAnnouncePromise, promise, "f");
try {
yield promise;
}
finally {
if (__classPrivateFieldGet(this, _WebTorrentClient_activeAnnouncePromise, "f") === promise) {
__classPrivateFieldSet(this, _WebTorrentClient_activeAnnouncePromise, null, "f");
}
}
});
}, _WebTorrentClient_createOffer = function _WebTorrentClient_createOffer() {
return __awaiter(this, void 0, void 0, function* () {
var _b, _c;
if (__classPrivateFieldGet(this, _WebTorrentClient_instances, "m", _WebTorrentClient_isDestroyed).call(this))
return undefined;
let pc;
try {
pc = new PeerConnection((_c = (_b = __classPrivateFieldGet(this, _WebTorrentClient_config, "f")).rtcConfig) === null || _c === void 0 ? void 0 : _c.call(_b));
__classPrivateFieldGet(this, _WebTorrentClient_negotiatingConnections, "f").add(pc);
const channel = pc.createDataChannel("webtorrent", __classPrivateFieldGet(this, _WebTorrentClient_config, "f").channelConfig);
const offer = yield safeCreateOffer(pc);
__classPrivateFieldGet(this, _WebTorrentClient_instances, "m", _WebTorrentClient_throwIfDestroyed).call(this);
yield safeSetLocalDescription(pc, offer);
__classPrivateFieldGet(this, _WebTorrentClient_instances, "m", _WebTorrentClient_throwIfDestroyed).call(this);
yield __classPrivateFieldGet(this, _WebTorrentClient_instances, "m", _WebTorrentClient_waitForIceGathering).call(this, pc);
__classPrivateFieldGet(this, _WebTorrentClient_instances, "m", _WebTorrentClient_throwIfDestroyed).call(this);
const sdp = pc.localDescription;
if (!(sdp === null || sdp === void 0 ? void 0 : sdp.sdp)) {
pc.close();
return undefined;
}
const offerId = generateOfferId();
__classPrivateFieldGet(this, _WebTorrentClient_pendingOffers, "f").set(offerId, {
connection: pc,
channel,
timeoutId: setTimeout(() => {
__classPrivateFieldGet(this, _WebTorrentClient_instances, "m", _WebTorrentClient_cleanupPendingOffer).call(this, offerId);
}, __classPrivateFieldGet(this, _WebTorrentClient_config, "f").offerTimeout()),
});
return {
offer: { type: sdp.type, sdp: sdp.sdp },
offer_id: offerId,
};
}
catch (err) {
pc === null || pc === void 0 ? void 0 : pc.close();
if (!__classPrivateFieldGet(this, _WebTorrentClient_instances, "m", _WebTorrentClient_isDestroyed).call(this)) {
__classPrivateFieldGet(this, _WebTorrentClient_eventTarget, "f").dispatchEvent("warning", new TrackerWarning("offer-failed", `Failed to create offer: ${err instanceof Error ? err.message : String(err)}`, err));
}
}
finally {
if (pc) {
__classPrivateFieldGet(this, _WebTorrentClient_negotiatingConnections, "f").delete(pc);
}
}
return undefined;
});
}, _WebTorrentClient_sendStopped = function _WebTorrentClient_sendStopped() {
if (__classPrivateFieldGet(this, _WebTorrentClient_wsClient, "f").state !== "connected")
return;
const payload = __classPrivateFieldGet(this, _WebTorrentClient_instances, "m", _WebTorrentClient_buildAnnouncePayload).call(this, {
numwant: 0,
offers: [],
event: "stopped",
});
try {
__classPrivateFieldGet(this, _WebTorrentClient_wsClient, "f").send(JSON.stringify(payload));
}
catch (_b) {
// Best-effort "stopped" notification
}
}, _WebTorrentClient_buildAnnouncePayload = function _WebTorrentClient_buildAnnouncePayload({ numwant, offers, event, }) {
const payload = {
action: "announce",
info_hash: __classPrivateFieldGet(this, _WebTorrentClient_config, "f").infoHash,
peer_id: __classPrivateFieldGet(this, _WebTorrentClient_config, "f").peerId,
numwant,
uploaded: 0,
downloaded: 0,
offers,
};
if (event) {
payload.event = event;
}
if (__classPrivateFieldGet(this, _WebTorrentClient_trackerId, "f")) {
payload.trackerid = __classPrivateFieldGet(this, _WebTorrentClient_trackerId, "f");
}
return payload;
}, _WebTorrentClient_handleIncomingOffer = function _WebTorrentClient_handleIncomingOffer(_b) {
return __awaiter(this, arguments, void 0, function* ({ sdp: offerSdp, peerId: remotePeerId, offerId: remoteOfferId, }) {
var _c, _d;
if (__classPrivateFieldGet(this, _WebTorrentClient_instances, "m", _WebTorrentClient_isDestroyed).call(this))
return;
if (!__classPrivateFieldGet(this, _WebTorrentClient_config, "f").claimPeer(remotePeerId)) {
return; // Reject offer silently
}
let pc;
try {
pc = new PeerConnection((_d = (_c = __classPrivateFieldGet(this, _WebTorrentClient_config, "f")).rtcConfig) === null || _d === void 0 ? void 0 : _d.call(_c));
__classPrivateFieldGet(this, _WebTorrentClient_negotiatingConnections, "f").add(pc);
yield safeSetRemoteDescription(pc, new SessionDescription(offerSdp));
__classPrivateFieldGet(this, _WebTorrentClient_instances, "m", _WebTorrentClient_throwIfDestroyed).call(this);
const answer = yield safeCreateAnswer(pc);
__classPrivateFieldGet(this, _WebTorrentClient_instances, "m", _WebTorrentClient_throwIfDestroyed).call(this);
yield safeSetLocalDescription(pc, answer);
__classPrivateFieldGet(this, _WebTorrentClient_instances, "m", _WebTorrentClient_throwIfDestroyed).call(this);
yield __classPrivateFieldGet(this, _WebTorrentClient_instances, "m", _WebTorrentClient_waitForIceGathering).call(this, pc);
__classPrivateFieldGet(this, _WebTorrentClient_instances, "m", _WebTorrentClient_throwIfDestroyed).call(this);
const sdp = pc.localDescription;
if (!sdp) {
throw new Error("Failed to get local description after ICE gathering");
}
const payload = {
action: "announce",
info_hash: __classPrivateFieldGet(this, _WebTorrentClient_config, "f").infoHash,
peer_id: __classPrivateFieldGet(this, _WebTorrentClient_config, "f").peerId,
to_peer_id: remotePeerId,
offer_id: remoteOfferId,
answer: { type: sdp.type, sdp: sdp.sdp },
};
__classPrivateFieldGet(this, _WebTorrentClient_wsClient, "f").send(JSON.stringify(payload));
const channel = yield __classPrivateFieldGet(this, _WebTorrentClient_instances, "m", _WebTorrentClient_waitForConnection).call(this, pc);
__classPrivateFieldGet(this, _WebTorrentClient_instances, "m", _WebTorrentClient_throwIfDestroyed).call(this);
__classPrivateFieldGet(this, _WebTorrentClient_eventTarget, "f").dispatchEvent("peerConnected", {
peerId: remotePeerId,
connection: pc,
channel,
});
}
catch (err) {
pc === null || pc === void 0 ? void 0 : pc.close();
// Always dispatch peerConnectFailed so the Manager can release the peer
// from #connectingPeers. Safe to call after destroy: event target is
// already cleared, making the dispatch a no-op.
__classPrivateFieldGet(this, _WebTorrentClient_eventTarget, "f").dispatchEvent("peerConnectFailed", {
peerId: remotePeerId,
error: new PeerConnectError("connection-failed", err instanceof Error ? err.message : String(err), err),
});
}
finally {
if (pc)
__classPrivateFieldGet(this, _WebTorrentClient_negotiatingConnections, "f").delete(pc);
}
});
}, _WebTorrentClient_handleIncomingAnswer = function _WebTorrentClient_handleIncomingAnswer(_b) {
return __awaiter(this, arguments, void 0, function* ({ sdp: answerSdp, peerId: remotePeerId, offerId: ourOfferId, }) {
if (__classPrivateFieldGet(this, _WebTorrentClient_instances, "m", _WebTorrentClient_isDestroyed).call(this))
return;
const pending = __classPrivateFieldGet(this, _WebTorrentClient_pendingOffers, "f").get(ourOfferId);
if (!pending)
return; // Offer expired or invalid
// Stop tracking it as pending
__classPrivateFieldGet(this, _WebTorrentClient_pendingOffers, "f").delete(ourOfferId);
clearTimeout(pending.timeoutId);
if (!__classPrivateFieldGet(this, _WebTorrentClient_config, "f").claimPeer(remotePeerId)) {
pending.connection.close();
return; // Reject answer silently
}
__classPrivateFieldGet(this, _WebTorrentClient_negotiatingConnections, "f").add(pending.connection);
try {
yield safeSetRemoteDescription(pending.connection, new SessionDescription(answerSdp));
__classPrivateFieldGet(this, _WebTorrentClient_instances, "m", _WebTorrentClient_throwIfDestroyed).call(this);
const channel = yield __classPrivateFieldGet(this, _WebTorrentClient_instances, "m", _WebTorrentClient_waitForConnection).call(this, pending.connection, pending.channel);
__classPrivateFieldGet(this, _WebTorrentClient_instances, "m", _WebTorrentClient_throwIfDestroyed).call(this);
__classPrivateFieldGet(this, _WebTorrentClient_eventTarget, "f").dispatchEvent("peerConnected", {
peerId: remotePeerId,
connection: pending.connection,
channel,
});
}
catch (err) {
pending.connection.close();
// Always dispatch peerConnectFailed so the Manager can release the peer
// from #connectingPeers. Safe to call after destroy: event target is
// already cleared, making the dispatch a no-op.
__classPrivateFieldGet(this, _WebTorrentClient_eventTarget, "f").dispatchEvent("peerConnectFailed", {
peerId: remotePeerId,
error: new PeerConnectError("connection-failed", err instanceof Error ? err.message : String(err), err),
});
}
finally {
__classPrivateFieldGet(this, _WebTorrentClient_negotiatingConnections, "f").delete(pending.connection);
}
});
}, _WebTorrentClient_waitForIceGathering = function _WebTorrentClient_waitForIceGathering(pc) {
return new Promise((resolve, reject) => {
if (pc.iceGatheringState === "complete") {
resolve();
return;
}
if (pc.signalingState === "closed") {
reject(new Error("RTCPeerConnection closed"));
return;
}
let timeoutId = undefined;
const cleanup = () => {
clearTimeout(timeoutId);
pc.removeEventListener("icegatheringstatechange", onGatheringChange);
pc.removeEventListener("icecandidate", onIceCandidate);
pc.removeEventListener("signalingstatechange", onSignalingChange);
__classPrivateFieldGet(this, _WebTorrentClient_destroyAbortController, "f").signal.removeEventListener("abort", onAbort);
};
const onGatheringChange = () => {
if (pc.iceGatheringState === "complete") {
cleanup();
resolve();
}
};
// A null candidate also signals gathering completion in some browsers
// more reliably than icegatheringstatechange.
const onIceCandidate = (event) => {
if (event.candidate === null) {
cleanup();
resolve();
}
};
const onSignalingChange = () => {
if (pc.signalingState === "closed") {
cleanup();
reject(new Error("RTCPeerConnection closed"));
}
};
const onAbort = () => {
cleanup();
reject(new Error("ICE gathering aborted due to teardown"));
};
if (__classPrivateFieldGet(this, _WebTorrentClient_destroyAbortController, "f").signal.aborted) {
onAbort();
return;
}
timeoutId = setTimeout(() => {
cleanup();
resolve(); // Use whatever candidates we have gathered so far
}, __classPrivateFieldGet(this, _WebTorrentClient_config, "f").iceGatheringTimeout());
pc.addEventListener("icegatheringstatechange", onGatheringChange);
pc.addEventListener("icecandidate", onIceCandidate);
pc.addEventListener("signalingstatechange", onSignalingChange);
__classPrivateFieldGet(this, _WebTorrentClient_destroyAbortController, "f").signal.addEventListener("abort", onAbort);
});
}, _WebTorrentClient_waitForConnection = function _WebTorrentClient_waitForConnection(pc, channel) {
const { promise, resolve, reject } = getPromiseWithResolvers();
let timeoutId = undefined;
let boundChannel = channel;
const rejectIfTerminalState = () => {
if (isTerminalConnectionState(pc.iceConnectionState)) {
cleanup();
reject(new Error(`ICE connection ${pc.iceConnectionState}`));
return true;
}
return false;
};
const onChannelOpen = () => {
cleanup();
if (boundChannel) {
resolve(boundChannel);
}
else {
reject(new Error("Data channel missing on open"));
}
};
const onChannelError = () => {
cleanup();
reject(new Error("Data channel error"));
};
const onChannelClose = () => {
cleanup();
reject(new Error("Data channel closed prematurely"));
};
const bindDataChannel = (dc) => {
boundChannel = dc;
if (dc.readyState === "open") {
onChannelOpen();
}
else if (dc.readyState === "closed" || dc.readyState === "closing") {
onChannelClose();
}
else {
dc.addEventListener("open", onChannelOpen);
dc.addEventListener("error", onChannelError);
dc.addEventListener("close", onChannelClose);
dc.addEventListener("closing", onChannelClose);
}
};
const onDataChannel = (event) => {
if (!boundChannel) {
bindDataChannel(event.channel);
}
};
const onAbort = () => {
cleanup();
reject(new Error("Connection aborted due to teardown"));
};
const cleanup = () => {
clearTimeout(timeoutId);
pc.removeEventListener("iceconnectionstatechange", rejectIfTerminalState);
pc.removeEventListener("datachannel", onDataChannel);
if (boundChannel) {
boundChannel.removeEventListener("open", onChannelOpen);
boundChannel.removeEventListener("error", onChannelError);
boundChannel.removeEventListener("close", onChannelClose);
boundChannel.removeEventListener("closing", onChannelClose);
}
__classPrivateFieldGet(this, _WebTorrentClient_destroyAbortController, "f").signal.removeEventListener("abort", onAbort);
};
if (__classPrivateFieldGet(this, _WebTorrentClient_destroyAbortController, "f").signal.aborted) {
onAbort();
return promise;
}
if (rejectIfTerminalState())
return promise;
timeoutId = setTimeout(() => {
cleanup();
reject(new Error("Data channel open timeout"));
}, __classPrivateFieldGet(this, _WebTorrentClient_config, "f").connectionTimeout());
pc.addEventListener("iceconnectionstatechange", rejectIfTerminalState);
__classPrivateFieldGet(this, _WebTorrentClient_destroyAbortController, "f").signal.addEventListener("abort", onAbort);
if (boundChannel) {
bindDataChannel(boundChannel);
}
else {
pc.addEventListener("datachannel", onDataChannel);
}
return promise;
}, _WebTorrentClient_cleanupPendingOffer = function _WebTorrentClient_cleanupPendingOffer(offerId, pending) {
const entry = pending !== null && pending !== void 0 ? pending : __classPrivateFieldGet(this, _WebTorrentClient_pendingOffers, "f").get(offerId);
if (entry) {
clearTimeout(entry.timeoutId);
entry.connection.close();
__classPrivateFieldGet(this, _WebTorrentClient_pendingOffers, "f").delete(offerId);
}
}, _WebTorrentClient_cleanupPendingOffers = function _WebTorrentClient_cleanupPendingOffers() {
for (const [offerId, pending] of __classPrivateFieldGet(this, _WebTorrentClient_pendingOffers, "f")) {
__classPrivateFieldGet(this, _WebTorrentClient_instances, "m", _WebTorrentClient_cleanupPendingOffer).call(this, offerId, pending);
}
}, _WebTorrentClient_cleanupNegotiatingConnections = function _WebTorrentClient_cleanupNegotiatingConnections() {
for (const pc of __classPrivateFieldGet(this, _WebTorrentClient_negotiatingConnections, "f")) {
pc.close();
}
__classPrivateFieldGet(this, _WebTorrentClient_negotiatingConnections, "f").clear();
};
_WebTorrentClient_DEFAULT_ANNOUNCE_INTERVAL_SECONDS = { value: 120 };
_WebTorrentClient_MIN_ANNOUNCE_INTERVAL_SECONDS = { value: 20 };
//# sourceMappingURL=index.js.map