fix(webtorrent): ensure magnet webseeds are used in stream

Extracts `ws` (web seed) parameters from the magnet link and merges them with the explicit `urlList` (e.g., from direct URL) when initializing the WebTorrent stream. This fixes a regression where magnet-embedded web seeds were ignored if a direct URL was provided.

- Updates `js/magnets.js` to parse `ws` parameters and export `webSeedsFromMagnet`.
- Updates `js/webtorrent.js` to merge magnet web seeds with `opts.urlList`.
- Adds regression test `tests/webtorrent-webseeds.test.mjs`.

Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
google-labs-jules[bot]andPR0M3TH3AN committed 2026-01-30 22:01:13 +00:00
1 parent 5b3db62513
commit 2bebaa6465
3 files changed
+113 -7

No files matched your search

+30 -3
View File
@@ -49,14 +49,36 @@ export function trackersFromMagnet(magnet) {
return Array.from(deduped);
}
export function webSeedsFromMagnet(magnet) {
if (typeof magnet !== "string") {
return [];
}
const parsed = parseMagnetLite(magnet);
if (!parsed || !Array.isArray(parsed.ws)) {
return [];
}
const deduped = new Set();
parsed.ws.forEach((url) => {
if (typeof url !== "string") {
return;
}
const trimmed = url.trim();
if (!trimmed) {
return;
}
deduped.add(trimmed);
});
return Array.from(deduped);
}
function parseMagnetLite(magnet) {
if (typeof magnet !== "string") {
return { infoHash: "", announce: [] };
return { infoHash: "", announce: [], ws: [] };
}
const trimmed = magnet.trim();
if (!trimmed) {
return { infoHash: "", announce: [] };
return { infoHash: "", announce: [], ws: [] };
}
const lower = trimmed.toLowerCase();
@@ -64,6 +86,7 @@ function parseMagnetLite(magnet) {
const paramsPart = queryIndex >= 0 ? trimmed.slice(queryIndex + 1) : "";
const params = paramsPart.split("&");
const announce = [];
const ws = [];
let infoHash = "";
for (const param of params) {
@@ -89,8 +112,12 @@ function parseMagnetLite(magnet) {
if (value) {
announce.push(value);
}
} else if (key === "ws" || key === "webseed") {
if (value) {
ws.push(value);
}
}
}
return { infoHash, announce };
return { infoHash, announce, ws };
}
+7 -4
View File
@@ -23,7 +23,7 @@ import { WSS_TRACKERS } from "./constants.js";
import { safeDecodeURIComponent } from "./utils/safeDecode.js";
import { devLogger, userLogger } from "./utils/logger.js";
import { emit } from "./embedDiagnostics.js";
import { infoHashFromMagnet } from "./magnets.js";
import { infoHashFromMagnet, webSeedsFromMagnet } from "./magnets.js";
const DEFAULT_PROBE_TRACKERS = Object.freeze([...WSS_TRACKERS]);
@@ -960,15 +960,18 @@ export class TorrentClient {
}
const isFirefoxBrowser = this.isFirefox();
const candidateUrls = Array.isArray(opts?.urlList)
const explicitUrls = Array.isArray(opts?.urlList)
? opts.urlList
.map((entry) => (typeof entry === "string" ? entry.trim() : ""))
.filter((entry) => /^https?:\/\//i.test(entry))
: [];
const magnetUrls = webSeedsFromMagnet(effectiveMagnetURI);
const combinedUrls = [...new Set([...explicitUrls, ...magnetUrls])];
const chromeOptions = { strategy: "sequential" };
if (candidateUrls.length) {
chromeOptions.urlList = candidateUrls;
if (combinedUrls.length) {
chromeOptions.urlList = combinedUrls;
}
return new Promise((resolve, reject) => {
+76
View File
@@ -0,0 +1,76 @@
import test from "node:test";
import assert from "node:assert/strict";
// Setup globals required by webtorrent.js
if (typeof self === 'undefined') global.self = global;
if (typeof navigator === 'undefined') {
global.navigator = {
userAgent: 'test',
serviceWorker: {
register: async () => ({ active: true }),
ready: Promise.resolve({ active: true }),
controller: { postMessage: () => {} },
addEventListener: () => {},
removeEventListener: () => {},
}
};
}
if (typeof window === 'undefined') {
global.window = {
location: { hostname: 'localhost', origin: 'http://localhost' },
isSecureContext: true,
navigator: global.navigator
}
}
if (typeof global.location === 'undefined') {
global.location = global.window.location;
}
// Import dynamic to ensure globals are set
const { TorrentClient } = await import('../js/webtorrent.js');
class MockWebTorrent {
constructor() {
this.torrents = [];
}
add(magnet, opts, cb) {
this.lastAdd = { magnet, opts };
const torrent = {
on: () => {},
files: [{name: 'video.mp4', streamTo: () => {}}],
once: () => {},
destroy: () => {},
};
if (cb) cb(torrent);
return torrent;
}
destroy() {}
createServer() {}
}
test("streamVideo merges magnet web seeds with explicit web seeds", async () => {
const client = new TorrentClient({ webTorrentClass: MockWebTorrent });
client.serviceWorkerDisabled = true;
const magnet = 'magnet:?xt=urn:btih:123&ws=http://magnet-seed.com/file.mp4';
const urlList = ['http://direct-url.com/video.mp4'];
const videoElement = {
play: async () => {},
addEventListener: () => {},
muted: false,
crossOrigin: null // streamVideo sets this
};
try {
await client.streamVideo(magnet, videoElement, { urlList });
} catch (e) {
console.log(e);
}
const lastAdd = client.client.lastAdd;
assert.ok(lastAdd.opts.urlList.includes('http://magnet-seed.com/file.mp4'), "Magnet web seed missing");
assert.ok(lastAdd.opts.urlList.includes('http://direct-url.com/video.mp4'), "Explicit web seed missing");
});