Skip to content

Instantly share code, notes, and snippets.

@smhmd
Created July 19, 2025 15:06
Show Gist options
  • Select an option

  • Save smhmd/0d4f7d18f9c0b81cb5c2103f3d020bb3 to your computer and use it in GitHub Desktop.

Select an option

Save smhmd/0d4f7d18f9c0b81cb5c2103f3d020bb3 to your computer and use it in GitHub Desktop.
import type { AnnounceResponse } from "./types";
import {
decodeHexStringToAscii,
convertBytesToHexString,
generateSecureRandomBytes,
} from "./utils";
const infoHash = "08ada5a7a6183aae1e09d831df6748d566095a10";
const peerId = "-WW0109-sC9Dl2bON8bO";
const announceUrl = "wss://tracker.openwebtorrent.com/";
const stunServers = [{ urls: "stun:stun.l.google.com:19302" }];
const numwant = 5;
const socket = new WebSocket(announceUrl);
const peers = new Map<string, RTCPeerConnection>();
const terminalStates = ["disconnected", "failed", "closed"];
socket.addEventListener("open", async () => {
const offers = await generateOffers(numwant);
socket.send(
JSON.stringify({
action: "announce",
event: "started",
info_hash: decodeHexStringToAscii(infoHash),
peer_id: peerId,
numwant,
offers,
})
);
});
socket.addEventListener("message", async (event) => {
const data = JSON.parse(event.data) as AnnounceResponse;
if (data.action !== "announce") return;
if (data.offer && data.peer_id && data.offer_id) {
const peer = new RTCPeerConnection({
iceServers: stunServers,
iceCandidatePoolSize: 10,
});
peer.ondatachannel = (event) => {
const channel = event.channel;
channel.onopen = () => {
// Immediately request metadata
channel.send(JSON.stringify({ type: "request-metadata" }));
};
channel.onmessage = (event) => {
try {
const msg = JSON.parse(event.data);
if (msg.type === "metadata") {
console.log(msg.data);
}
} catch (e) {
console.warn("Invalid message from peer:", event.data);
}
};
};
peer.onicecandidate = (event) => {
if (event.candidate === null) {
socket.send(
JSON.stringify({
action: "announce",
info_hash: decodeHexStringToAscii(infoHash),
peer_id: peerId,
to_peer_id: data.peer_id,
answer: peer.localDescription,
offer_id: data.offer_id,
})
);
}
};
await peer.setRemoteDescription(data.offer);
const answer = await peer.createAnswer();
await peer.setLocalDescription(answer);
peer.onconnectionstatechange = () => {
if (terminalStates.includes(peer.connectionState)) {
peers.delete(data.offer_id);
}
};
peers.set(data.offer_id, peer);
}
if (data.answer && data.peer_id && data.offer_id) {
const peer = peers.get(data.offer_id);
if (peer) {
await peer.setRemoteDescription(data.answer);
}
}
});
async function generateOffers(count: number) {
return await Promise.all(
Array.from({ length: count }, async () => {
const peer = new RTCPeerConnection({
iceServers: stunServers,
iceCandidatePoolSize: 10,
});
const channel = peer.createDataChannel("metadata");
channel.onopen = () => {
// Immediately request metadata
channel.send(JSON.stringify({ type: "request-metadata" }));
};
channel.onmessage = (event) => {
try {
const msg = JSON.parse(event.data);
if (msg.type === "metadata") {
console.log(msg.data);
}
} catch (e) {
console.warn("Invalid message from peer:", event.data);
}
};
const offerId = convertBytesToHexString(generateSecureRandomBytes(20));
const offer = await peer.createOffer();
await peer.setLocalDescription(offer);
peers.set(offerId, peer);
peer.onconnectionstatechange = () => {
if (terminalStates.includes(peer.connectionState)) {
peers.delete(offerId);
}
};
return {
offer,
offer_id: offerId,
};
})
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment