bitvid attached every hosted source with `videoEl.src = url`, which only plays
HLS on Safari-family browsers. Firefox 146 fails with MEDIA_ERR_SRC_NOT_SUPPORTED
(canPlayType returns ""), so every .m3u8 note -- e.g. anything published by
nostube/slidestr -- was unplayable there and the URL-health badge showed
"offline". Chrome/Brave 149 happen to play HLS natively, which is why this
looked browser-specific.
Vendor hls.js (npm run build:hls) and route HLS sources through it:
- js/services/hlsPlayback.js owns detection, engine choice and the hls.js
lifecycle. Engine choice keys off MediaSource, NOT canPlayType: Chrome
returns "maybe" for every HLS type, so a native-first branch would hang.
Native is used only when MSE is genuinely unavailable.
- The bundle is the FULL build, not hls.js/light -- light drops the alternate
audio stream controller and real ladders demux audio into an EXT-X-MEDIA
group. It is lazy-imported only when a .m3u8 is encountered, so ordinary
MP4/WebTorrent playback never pays the ~500KB.
- modalCoordinator tears the instance down first; clearing `src` alone leaves
the transmuxing worker and segment fetches running after the modal closes.
- urlHealthController probes HLS by fetching the playlist for #EXTM3U. The old
bare-<video> probe could not load it and on Chrome emitted no events at all,
burning the full timeout on every HLS card.
- HLS_PLAYBACK_START_TIMEOUT widens the 3s start window, which fired
mid-startup: the bundle, master playlist, variant playlist, init segment and
first media segment are four-plus serial round trips.
Also fixes two bugs found while verifying this:
- A stale MediaError was reported as the current failure. Teardown clears `src`
and calls load(), leaving MEDIA_ERR_SRC_NOT_SUPPORTED ("Empty src attribute")
on the element; a later watchdog fallback rendered that as "Hosted playback
failed: source not supported or blocked." over a stream that was playing
fine. Only a MediaError produced by the current attempt is surfaced now.
Sources are also bound before the watchdogs are armed, so hls.js's own
attachMedia() src swap cannot read as a playback failure.
- `audio/x-mpegurl` is a legacy alias for an HLS playlist of any kind, not an
audio-only assertion. isAudioOnlyVideoObject misfiled HLS video ladders
published with it as podcasts and dropped them from every video feed.
Verified end-to-end in Chromium, Chrome 149, Brave 149 and Firefox 146 against
a live HLS stream: Firefox goes from "No playable source found." to playing.
Rollback: stop calling attachHlsSource() and the previous `src` assignment
returns; see docs/playback-fallback.md.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Two torrent-path robustness fixes surfaced while testing WebTorrent peering:
- streamVideo now sets announce: [...WSS_TRACKERS]. A .torrent-buffer add (Blossom companion piece-map) has an EMPTY embedded announce list, so without this it reached no tracker, found no WebRTC peers, and streamed webseed-only (looked like a stuck '1 peer'). WebTorrent merges these with any trackers already on the source.
- A forced-torrent playback now falls back to the hosted URL when the torrent DEFINITIVELY fails (e.g. a transient companion-lookup failure under relay congestion, or a dead swarm) instead of hard-erroring 'No playable source found'. Only triggers on real failure, not a slow swarm. Rollback noted inline.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
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]>
The liveness probe already tries all of a video's sources, but actual playback
only ever tried the single primary URL and then fell straight to WebTorrent. Now
playback fails over across hosted mirrors first.
- PlaybackSession builds an ordered, de-duped hostedSourceCandidates (primary URL
first, then the video's imeta `sources`; accepts string or {url} entries).
- attemptHostedPlayback takes a candidateUrl; the url-first execution loop tries
each candidate and advances to the next mirror on probe-fail/stall/timeout,
cleaning up watchdogs + resetting the <video> between candidates, only calling
WebTorrent after every hosted mirror fails.
- `sources` plumbed coordinator -> strategy -> session. Single-source videos
(no `sources`) behave exactly as before.
Tests: tests/services/playbackService_multisource.test.mjs (candidate
construction; the full async fail-over flow is covered by the existing
playbackService_order suite in CI — that mock-timers harness doesn't run in this
sandbox). ROLLBACK: revert the loop to the single attemptHostedPlayback() call
and drop the sources plumbing.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
File-size decomposition (no behavior change). Move the self-contained
SimpleEventEmitter and extractWebSeedsFromMagnet (+ its WEBSEED_PARAM_KEYS and
the now-sole safeDecodeURIComponent consumer) out of playbackService.js into a
new js/services/playbackHelpers.js and import them back.
playbackService.js: 1350 -> 1274 lines (now under its original 1289 baseline).
Re-recorded the smaller baseline in check-file-size.mjs to lock the reduction.
Build + full unit suite green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
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]>
Two fixes for hosted (CDN) playback that users hit on a torrent-first instance:
1. Reliability: clear crossOrigin before direct hosted playback (in
resetVideoElement and right before video.src = httpsUrl). A leftover
crossOrigin="anonymous" from a prior WebTorrent attempt on the reused
<video> forced a CORS request that CDNs / redirect targets (e.g. archive.org
storage nodes) reject, failing with MEDIA_ERR_SRC_NOT_SUPPORTED ("source not
supported or blocked"). Direct <video> playback never needs CORS, so any URL
now plays.
2. CDN-first for dual-source: when a hosted URL exists, play it first for
instant, reliable startup; WebTorrent is reserved for URL-less videos (and
explicit forcedSource: "torrent"). Previously a torrent-first instance made
dual-source videos wait on a P2P cold start before falling back to the CDN.
The torrent-first branch is now only reached without a URL, so it waits
indefinitely for peers (no plan B) rather than on a fixed timeout.
Tests: hosted playback clears crossOrigin and plays; a torrent-first instance
plays the URL first when one exists and never calls WebTorrent; URL-less videos
still use WebTorrent. Spec-corrected two playbackService_order tests that
encoded the old torrent-first-for-dual-source behavior. All mutation-verified.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
When a video has no fallback URL (CDN), the playback service now waits indefinitely for peers instead of timing out after 3 seconds. This prevents premature failures for WebTorrent-only content.
Ref: `js/services/playbackService.js`
Co-authored-by: PR0M3TH3AN <[email protected]>
Introduces a race condition between the primary playback source (URL or Torrent) and a configurable timeout. If the primary source fails to reach the 'playing' state within the timeout window, the system automatically falls back to the alternative source.
- Adds `DEFAULT_PLAYBACK_START_TIMEOUT` to instance config (3000ms).
- Updates `PlaybackService` to implement timeout logic using `withTimeout` wrapper.
- Ensures proper cleanup of stalled attempts (resetting video element, destroying torrent client).
- Added `DEFAULT_PLAYBACK_SOURCE` to `config/instance-config.js` to allow choosing between "url" (default) and "torrent" priority.
- Updated `js/constants.js` to derive `URL_FIRST_ENABLED` flag from the configuration.
- Refactored `PlaybackService.execute()` in `js/services/playbackService.js` to robustly support "Torrent First" logic with fallback to URL, while maintaining "URL First" with fallback to Torrent.
- Added unit tests in `tests/services/playbackService_order.test.mjs` to verify playback order and fallback mechanics.
- Added source toggle UI in `components/video-modal.html`.
- Updated `VideoModal.js` to manage toggle visibility (only when both URL and Magnet exist), update peer/server counts, and dispatch `playback:switch-source` events.
- Updated `PlaybackService.js` to support a `forcedSource` option, allowing bypassing of the URL probe when "torrent" is forced.
- Updated `app.js` to listen for the toggle event and re-initialize playback with the forced source preference.
- Updated `tests/ui/helpers/jsdom-test-helpers.mjs` to expose necessary globals for JSDOM testing.
- Verified functionality with targeted updates and existing unit tests.