playback: fix false "No playable source found." on URL-only videos

A bitvid (kind-30078) video with a perfectly playable hosted URL (verified:
archive.org 200 video/mp4 with byte ranges) failed with "No playable source
found." — the video element's src was never even set.

Root cause: the hosted-URL probe (fetch HEAD, mode:no-cors) runs BEFORE src is
assigned, inside the playback-start window — but URL_PROBE_TIMEOUT_MS (4s) is
LONGER than PLAYBACK_START_TIMEOUT (3s). A slow no-cors HEAD (archive.org, and
any host that doesn't answer HEAD fast) consumed the whole start budget, the
withTimeout wrapper aborted the attempt, and the URL was never loaded. A
`<video>` element plays cross-origin URLs WITHOUT CORS anyway, so the fetch
probe was both unnecessary and actively harmful here.

Fix:
- Skip the probe entirely when there's no magnet (URL-only) — nothing to fail
  over to, so the video element loads the URL directly. This is the reported
  case; verified live: the video now plays (readyState 4, currentTime advancing).
- An ambiguous/error probe no longer VETOES the URL (only a definitively-read
  "bad" status deprioritizes it, and even then only when a magnet fallback
  exists) — the video element + watchdogs are the real arbiter.

Tests: spec-corrected the always-probe assertion to a dual-source case + added
a URL-only skip-probe test (tests/services/playbackService.test.mjs). 17 pass.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Fable 5 committed 2026-07-05 00:13:14 -04:00
1 parent 5a7481e30e
commit 68bdee8da5
2 files changed
+88 -7

No files matched your search

+26 -6
View File
@@ -988,15 +988,35 @@ class PlaybackSession extends SimpleEventEmitter {
{ once: true }
);
const probeResult = await this.service.probeHostedUrl({
url: candidateUrl,
magnet: this.trimmedMagnet,
probeUrl,
});
// The probe (fetch HEAD) only helps DECIDE whether to fail over to P2P
// fast. When the URL is the only source there's nothing to fail over
// to — and the probe's timeout (URL_PROBE_TIMEOUT_MS, 4s) is LONGER than
// the playback-start window (PLAYBACK_START_TIMEOUT, 3s), so a slow
// no-cors HEAD (e.g. archive.org) would consume the whole budget and the
// attempt would time out before `src` is ever set → a false "No playable
// source found." So skip the probe when there's no magnet and let the
// video element (the real arbiter) load the URL directly.
const probeResult =
this.magnetForPlayback
? await this.service.probeHostedUrl({
url: candidateUrl,
magnet: this.trimmedMagnet,
probeUrl,
})
: { outcome: "skipped-url-only" };
const probeOutcome = probeResult?.outcome || "error";
const probeStatus = probeResult?.status || "unknown";
// A `<video>` element plays cross-origin URLs WITHOUT CORS, but this
// probe uses fetch (CORS-limited): a benign no-cors HEAD failure or a
// slow HEAD returns "error"/"opaque" for perfectly playable hosts (e.g.
// archive.org — 200 video/mp4 with byte ranges). An ambiguous probe must
// NEVER veto the URL into "No playable source found." Only a
// definitively-READ failure status ("bad") deprioritizes it — and even
// then, if there's no P2P fallback, we still try (a real media error is
// clearer than a false "no source"). The video element + watchdogs are
// the actual arbiter of playability.
const shouldAttemptHosted =
probeOutcome !== "bad" && probeOutcome !== "error";
probeOutcome !== "bad" || !this.magnetForPlayback;
this.service.log(
`[playVideoWithFallback] Hosted URL probe outcome=${probeOutcome} status=${probeStatus} shouldAttemptHosted=${shouldAttemptHosted}`