diff --git a/README.md b/README.md index 6f1ee1fd..28c05a43 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ Hosted URLs remain the preferred playback path, and you can still add a magnet o 2. **WebTorrent fallback**: If the URL probe fails or returns an error status, bitvid falls back to WebTorrent using the raw magnet. The helpers append HTTPS `ws=`/`xs=` hints so peers seed quickly. 3. **Safety checks**: Magnets are decoded with `safeDecodeMagnet()` and normalized via `normalizeAndAugmentMagnet()` before reaching WebTorrent. Trackers remain WSS-only to satisfy browser constraints. 4. **Operator playbook**: If a deployment causes playback regressions, flip the relevant feature flags back to their default values in `js/constants.js` and redeploy. Capture the rollback steps in AGENTS.md and the PR description so the Main channel stays stable. +5. **Deep dive**: See [`docs/playback-fallback.md`](docs/playback-fallback.md) for the call flow into `playbackService`, magnet normalization details, and fallback hand-off points. ### Watch history & view counts diff --git a/docs/playback-fallback.md b/docs/playback-fallback.md new file mode 100644 index 00000000..ff7ccea1 --- /dev/null +++ b/docs/playback-fallback.md @@ -0,0 +1,94 @@ +# URL-first playback fallback orchestration + +This document explains how `bitvidApp.playVideoWithFallback` in `js/app.js` coordinates hosted URL playback first and falls back to WebTorrent when needed. It also covers the supporting helpers in `js/services/playbackService.js`, `js/playbackUtils.js`, and `js/magnetUtils.js`, including how tracker lists and `ws=`/`xs=` hints are applied. + +## High-level flow + +1. `bitvidApp.playVideoWithFallback({ url, magnet })` trims inputs, ensures the video modal is ready, and constructs a playback session via `playbackService.createSession(...)`. +2. The session (from `js/services/playbackService.js`) derives a canonical magnet payload using `deriveTorrentPlaybackConfig` (from `js/playbackUtils.js`). +3. Playback starts with the hosted URL when `URL_FIRST_ENABLED` is true and a URL is available; otherwise, it goes directly to torrent playback. +4. If URL playback stalls or errors, the session triggers `playViaWebTorrent` with the normalized magnet and optional web seed hints. + +## Call flow into playbackService + +`bitvidApp.playVideoWithFallback` performs UI preparation and delegates the decision-making to a `PlaybackSession`: + +- **Session setup** + - `playbackService.createSession({ url, magnet, videoElement, probeUrl, playViaWebTorrent, ... })` + - The session captures `url`, `magnet`, and a request signature to de-duplicate duplicate calls. +- **Hosted URL probe** + - `PlaybackSession.execute()` calls `probeUrl(url)` to check availability and decides whether to attempt direct playback. +- **Hosted URL playback** + - On a successful probe, the session assigns `video.src = url` and calls `video.play()`. + - It registers watchdogs (`registerUrlPlaybackWatchdogs`) to detect stalls, errors, or aborts. +- **Fallback to WebTorrent** + - If watchdogs trigger or direct playback fails, `PlaybackSession` calls `playViaWebTorrent(magnet, { fallbackMagnet, urlList })`. + - `urlList` includes the hosted URL (if present) so it becomes a WebTorrent web seed. + +## Magnet helpers and normalization + +`playbackService` doesn’t manipulate magnets directly. It relies on `deriveTorrentPlaybackConfig` in `js/playbackUtils.js`, which does the following: + +- Calls `safeDecodeMagnet()` from `js/magnetUtils.js` so URL-encoded magnets become raw `magnet:?xt=urn:btih:...` strings. +- Accepts bare info hashes and promotes them to full magnet URIs when needed. +- Runs `normalizeAndAugmentMagnet()` (from `js/magnetUtils.js`) to: + - Append `ws=` web seed hints when a hosted URL is present. + - Append `xs=` torrent hints when supplied. + - Ensure WSS-only tracker URLs are present. + +The result is a canonical magnet used by WebTorrent, plus an optional `fallbackMagnet` that preserves the original raw input if normalization changed it. + +## Tracker lists, ws/xs hints, and constants + +Tracker and hint behavior is centralized in `js/constants.js` and `js/magnetUtils.js`: + +- `WSS_TRACKERS` in `js/constants.js` is the canonical browser-safe tracker list. `normalizeAndAugmentMagnet()` merges this list into the magnet if trackers are missing. +- `normalizeAndAugmentMagnet()` also applies `ws=` web seed hints (from the hosted URL) and `xs=` torrent hints (from user input), enforcing HTTPS when appropriate. +- `URL_FIRST_ENABLED` (also in `js/constants.js`) controls whether the hosted URL probe is attempted before torrent fallback. + +## Failure detection and hand-off points + +The fallback hand-off happens in these places inside `PlaybackSession.execute()`: + +1. **URL probe fails**: `probeUrl(url)` reports a bad outcome → session skips direct playback and goes straight to WebTorrent. +2. **Playback errors**: `video.play()` throws or fires `error` → session triggers the fallback. +3. **Playback stalls**: the watchdog timer (`registerUrlPlaybackWatchdogs`) detects `stalled`, `abort`, or no progress → session triggers the fallback. +4. **Autoplay blocked**: the browser blocks autoplay → session waits for a user gesture, re-enables watchdogs, and only falls back if playback still fails. + +## Sequence diagram + +```mermaid +sequenceDiagram + participant App as bitvidApp (js/app.js) + participant Service as PlaybackService (js/services/playbackService.js) + participant Session as PlaybackSession + participant Video as HTMLVideoElement + participant Magnet as magnetUtils/playbackUtils + + App->>Service: createSession({ url, magnet, videoElement, ... }) + Service->>Session: new PlaybackSession(...) + Session->>Magnet: deriveTorrentPlaybackConfig() + Magnet-->>Session: normalized magnet + fallbackMagnet + + Session->>App: showModalWithPoster() + Session->>App: probeUrl(url) + alt URL probe ok + URL_FIRST_ENABLED + Session->>Video: src=url; play() + Session->>Session: registerUrlPlaybackWatchdogs() + alt hosted playback succeeds + Session-->>App: sourcechange("url") + else watchdog/error triggers + Session->>App: playViaWebTorrent(magnet, { urlList }) + Session-->>App: sourcechange("torrent") + end + else URL probe fails or URL-first disabled + Session->>App: playViaWebTorrent(magnet, { urlList }) + Session-->>App: sourcechange("torrent") + end +``` + +## Implementation notes + +- The playback session is responsible for cleanup and emits events (`status`, `sourcechange`, `error`, `finished`) that `bitvidApp` listens to for UI updates. +- `bitvidApp.playVideoWithFallback` keeps the current video card and modal state in sync with the normalized magnet values so copy actions and stats reflect the actual playback payload. +