mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
[playback] stop the WebTorrent webseed flood on multi-file torrents
The video's hosted CDN URL was added to WebTorrent's `urlList`, but a standalone hosted-file URL is only a valid BEP19 webseed for a SINGLE-file torrent. For a multi-file torrent WebTorrent treats it as a base directory and appends "<torrent name>/<file path>", producing a doubled URL that 404s on every request — the flood seen on e.g. archive.org videos. The CDN URL is no longer placed in urlList. It's forwarded separately as `hostedUrlWebSeed` and attached post-metadata (handleTorrentStream) via torrent.addWebSeed ONLY when the torrent is a single file, where the URL maps directly to the file. Multi-file torrents get no file-pointing webseed, so no flood. Magnet-declared `ws=` webseeds are unaffected (passed via urlList as before). With CDN-first, dual-source videos play the CDN directly anyway, so this only affects forced-torrent / URL-less paths. Tests: a single-file torrent receives the hosted URL via addWebSeed; a multi-file torrent does not. The single-file guard is mutation-verified. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
ff5c641951
commit
006403b7b4
4 files changed
+89
-6
No files matched your search
@@ -362,7 +362,7 @@ export function createPlaybackCoordinator(deps) {
|
||||
|
||||
async playViaWebTorrent(
|
||||
magnet,
|
||||
{ fallbackMagnet = "", urlList = [] } = {}
|
||||
{ fallbackMagnet = "", urlList = [], hostedUrlWebSeed = "" } = {}
|
||||
) {
|
||||
const sanitizedUrlList = Array.isArray(urlList)
|
||||
? urlList
|
||||
@@ -371,6 +371,11 @@ export function createPlaybackCoordinator(deps) {
|
||||
)
|
||||
.filter((entry) => /^https?:\/\//i.test(entry))
|
||||
: [];
|
||||
const sanitizedHostedWebSeed =
|
||||
typeof hostedUrlWebSeed === "string" &&
|
||||
/^https?:\/\//i.test(hostedUrlWebSeed.trim())
|
||||
? hostedUrlWebSeed.trim()
|
||||
: "";
|
||||
|
||||
const attemptStream = async (candidate) => {
|
||||
const trimmedCandidate =
|
||||
@@ -418,6 +423,7 @@ export function createPlaybackCoordinator(deps) {
|
||||
this.modalVideo,
|
||||
{
|
||||
urlList: sanitizedUrlList,
|
||||
hostedUrlWebSeed: sanitizedHostedWebSeed,
|
||||
hooks: {
|
||||
// Surface a mid-stream stall instead of silently freezing on a
|
||||
// buffering frame: tell the user whether the swarm is just slow
|
||||
|
||||
@@ -868,10 +868,12 @@ class PlaybackSession extends SimpleEventEmitter {
|
||||
webSeedCandidates.push(trimmed);
|
||||
};
|
||||
|
||||
if (httpsUrl) {
|
||||
addWebSeedCandidate(httpsUrl);
|
||||
}
|
||||
|
||||
// NOTE: the hosted CDN URL is intentionally NOT added here. As a
|
||||
// WebTorrent `urlList` entry it is only valid for single-file torrents;
|
||||
// for multi-file torrents WebTorrent appends the torrent's internal path
|
||||
// and floods the network with doubled, 404-ing requests. It is forwarded
|
||||
// separately as `hostedUrlWebSeed` and attached post-metadata only when
|
||||
// the torrent is a single file (see TorrentClient.handleTorrentStream).
|
||||
const magnetWebSeeds = extractWebSeedsFromMagnet(this.magnetForPlayback);
|
||||
if (magnetWebSeeds.length > 0) {
|
||||
magnetWebSeeds.forEach((seed) => addWebSeedCandidate(seed));
|
||||
@@ -948,6 +950,7 @@ class PlaybackSession extends SimpleEventEmitter {
|
||||
torrentInstance = await playViaWebTorrent(this.magnetForPlayback, {
|
||||
fallbackMagnet: this.fallbackMagnet,
|
||||
urlList: webSeedCandidates,
|
||||
hostedUrlWebSeed: httpsUrl,
|
||||
});
|
||||
} catch (err) {
|
||||
const errorMessage = getTorrentErrorMessage(err);
|
||||
|
||||
+34
-1
@@ -796,6 +796,12 @@ export class TorrentClient {
|
||||
hooks && Number.isFinite(hooks.stallMs) && hooks.stallMs > 0
|
||||
? hooks.stallMs
|
||||
: TORRENT_PLAYBACK_STALL_MS;
|
||||
const hostedUrlWebSeed =
|
||||
hooks &&
|
||||
typeof hooks.hostedUrlWebSeed === "string" &&
|
||||
/^https?:\/\//i.test(hooks.hostedUrlWebSeed.trim())
|
||||
? hooks.hostedUrlWebSeed.trim()
|
||||
: "";
|
||||
|
||||
// Chrome-specific: Prune demo web seeds/trackers that chronically trip Chromium CORS
|
||||
// and deliberately mutate `torrent._opts` as a sanctioned WebTorrent workaround.
|
||||
@@ -853,6 +859,26 @@ export class TorrentClient {
|
||||
}
|
||||
}
|
||||
|
||||
// Attach the hosted CDN URL as a webseed ONLY for single-file torrents.
|
||||
// A standalone hosted-file URL is a valid BEP19 webseed only when the
|
||||
// torrent is a single file (the URL maps directly to it). For a multi-file
|
||||
// torrent, WebTorrent treats the URL as a base directory and appends
|
||||
// "<torrent name>/<file path>", producing a doubled URL that 404s on every
|
||||
// request — the webseed flood. Adding it post-metadata (when files.length
|
||||
// is known) avoids that entirely. Magnet-declared `ws=` webseeds are passed
|
||||
// separately via urlList and untouched.
|
||||
if (
|
||||
hostedUrlWebSeed &&
|
||||
files.length === 1 &&
|
||||
typeof torrent.addWebSeed === "function"
|
||||
) {
|
||||
try {
|
||||
torrent.addWebSeed(hostedUrlWebSeed);
|
||||
} catch (err) {
|
||||
this.log(`Failed to attach hosted webseed (${context} path):`, err);
|
||||
}
|
||||
}
|
||||
|
||||
// Satisfy autoplay requirements and keep cross-origin chunks usable (e.g., for snapshots).
|
||||
videoElement.crossOrigin = "anonymous";
|
||||
|
||||
@@ -1106,7 +1132,14 @@ export class TorrentClient {
|
||||
|
||||
const isFirefoxBrowser = this.isFirefox();
|
||||
const streamHooks =
|
||||
opts && typeof opts.hooks === "object" && opts.hooks ? opts.hooks : {};
|
||||
opts && typeof opts.hooks === "object" && opts.hooks
|
||||
? { ...opts.hooks }
|
||||
: {};
|
||||
// Hosted CDN URL to use as a single-file webseed (handleTorrentStream
|
||||
// attaches it post-metadata, only when the torrent is a single file).
|
||||
if (typeof opts?.hostedUrlWebSeed === "string" && opts.hostedUrlWebSeed) {
|
||||
streamHooks.hostedUrlWebSeed = opts.hostedUrlWebSeed;
|
||||
}
|
||||
const candidateUrls = Array.isArray(opts?.urlList)
|
||||
? opts.urlList
|
||||
.map((entry) => (typeof entry === "string" ? entry.trim() : ""))
|
||||
|
||||
@@ -293,4 +293,45 @@ describe("TorrentClient streaming hardening", () => {
|
||||
"previous streams' error listeners are torn down (no accumulation)",
|
||||
);
|
||||
});
|
||||
|
||||
it("attaches the hosted CDN URL as a webseed for a SINGLE-file torrent", () => {
|
||||
const client = freshClient();
|
||||
const torrent = new MultiFileTorrent([
|
||||
{ name: "v.mp4", length: 1000, streamTo: (el) => { el.src = "blob:x"; } },
|
||||
]);
|
||||
const added = [];
|
||||
torrent.addWebSeed = (url) => added.push(url);
|
||||
const videoElement = new MockVideoElement();
|
||||
|
||||
client.handleTorrentStream(torrent, videoElement, () => {}, () => {}, "chrome", {
|
||||
hostedUrlWebSeed: "https://cdn.example.com/v.mp4",
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
added,
|
||||
["https://cdn.example.com/v.mp4"],
|
||||
"a single-file torrent gets the hosted URL as a webseed",
|
||||
);
|
||||
});
|
||||
|
||||
it("does NOT attach the hosted CDN URL for a MULTI-file torrent (no doubled-URL flood)", () => {
|
||||
const client = freshClient();
|
||||
const torrent = new MultiFileTorrent([
|
||||
{ name: "feature.mp4", length: 100000, streamTo: (el) => { el.src = "blob:x"; } },
|
||||
{ name: "extra.mp4", length: 100 },
|
||||
]);
|
||||
const added = [];
|
||||
torrent.addWebSeed = (url) => added.push(url);
|
||||
const videoElement = new MockVideoElement();
|
||||
|
||||
client.handleTorrentStream(torrent, videoElement, () => {}, () => {}, "chrome", {
|
||||
hostedUrlWebSeed: "https://cdn.example.com/feature.mp4",
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
added.length,
|
||||
0,
|
||||
"a multi-file torrent must NOT get a file-pointing webseed (it would double + 404)",
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user