mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-31 01:42:27 +00:00
Merge pull request #1243 from PR0M3TH3AN/docs/playback-service-comments-12088992108818376989
Add developer documentation to playback services
This commit is contained in:
2 files changed
+104
-1
No files matched your search
@@ -1,6 +1,23 @@
|
||||
import { userLogger } from "../utils/logger.js";
|
||||
// js/services/playbackService.js
|
||||
|
||||
/**
|
||||
* PlaybackService acts as the central conductor for video playback.
|
||||
* It manages the lifecycle of a "PlaybackSession", which encapsulates the complex
|
||||
* logic of attempting to play a video from a direct URL (HTTP/CDN) first, and
|
||||
* falling back to a WebTorrent (P2P) stream if the URL is unreachable or stalls.
|
||||
*
|
||||
* Key Responsibilities:
|
||||
* - Singleton-ish orchestration (only one active session at a time usually).
|
||||
* - "Watchdog" monitoring: It attaches listeners to the <video> element to detect
|
||||
* frozen playback (stalls) or errors.
|
||||
* - Fallback logic: If the direct URL fails (404, network error) or playback
|
||||
* stalls, it seamlessly triggers the P2P engine (WebTorrent).
|
||||
* - Race Condition Management: Uses "request signatures" to ensure that if the user
|
||||
* clicks "Play" on a new video while the previous one is still loading, the
|
||||
* old session is cancelled and doesn't overwrite the new one.
|
||||
*/
|
||||
|
||||
class SimpleEventEmitter {
|
||||
constructor(logger = null) {
|
||||
this.logger = typeof logger === "function" ? logger : null;
|
||||
@@ -116,6 +133,22 @@ export class PlaybackService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a set of event listeners ("watchdogs") on the video element to detect
|
||||
* when playback has stalled or failed.
|
||||
*
|
||||
* This is critical for the "Hybrid" playback strategy. We attempt to play the
|
||||
* direct URL (which is cheap and fast). If it hangs (e.g., buffering forever)
|
||||
* or errors out, this watchdog fires the `onFallback` callback, which triggers
|
||||
* the switch to P2P.
|
||||
*
|
||||
* @param {HTMLVideoElement} videoElement
|
||||
* @param {object} options
|
||||
* @param {number} options.stallMs - How long to wait without progress before considering it a "stall".
|
||||
* @param {Function} options.onSuccess - Called if playback starts successfully (clears the watchdog).
|
||||
* @param {Function} options.onFallback - Called if playback fails or stalls.
|
||||
* @returns {Function} cleanup function to remove listeners.
|
||||
*/
|
||||
registerUrlPlaybackWatchdogs(
|
||||
videoElement,
|
||||
{ stallMs = 8000, onSuccess, onFallback } = {}
|
||||
@@ -171,6 +204,8 @@ export class PlaybackService {
|
||||
}
|
||||
};
|
||||
|
||||
// The timer is reset on any "progress" event. If `stallMs` passes without
|
||||
// a reset, we assume the connection is dead/stalled and fallback.
|
||||
const resetTimer = () => {
|
||||
if (!active || !normalizedStallMs) {
|
||||
return;
|
||||
@@ -186,12 +221,16 @@ export class PlaybackService {
|
||||
listeners.push([eventName, handler]);
|
||||
};
|
||||
|
||||
// Immediate failure conditions
|
||||
addListener("error", () => triggerFallback("error"));
|
||||
addListener("abort", () => triggerFallback("abort"));
|
||||
addListener("stalled", () => triggerFallback("stalled"));
|
||||
|
||||
// Success conditions
|
||||
addListener("playing", handleSuccess);
|
||||
addListener("ended", handleSuccess);
|
||||
|
||||
// Keep-alive events that reset the stall timer
|
||||
const timerEvents = [
|
||||
"timeupdate",
|
||||
"progress",
|
||||
@@ -228,6 +267,19 @@ export class PlaybackService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PlaybackSession represents a single attempt to play a specific video.
|
||||
* It manages the state machine of:
|
||||
* 1. Probing the direct URL.
|
||||
* 2. Attempting to play the URL.
|
||||
* 3. Monitoring for stalls.
|
||||
* 4. Falling back to WebTorrent if needed.
|
||||
*
|
||||
* It uses a `requestSignature` (JSON of url+magnet) to uniquely identify the request.
|
||||
* This is used by the UI layer to prevent race conditions: if the user clicks a new video,
|
||||
* the UI can check `matchesRequestSignature` to see if it can reuse the active session
|
||||
* or if it needs to spin up a new one.
|
||||
*/
|
||||
class PlaybackSession extends SimpleEventEmitter {
|
||||
constructor(service, options = {}) {
|
||||
super((message, ...args) => service.log(message, ...args));
|
||||
@@ -242,6 +294,9 @@ class PlaybackSession extends SimpleEventEmitter {
|
||||
typeof options.url === "string" ? options.url.trim() : "";
|
||||
this.trimmedMagnet =
|
||||
typeof options.magnet === "string" ? options.magnet.trim() : "";
|
||||
|
||||
// The signature prevents duplicate work if the same video is requested again
|
||||
// while already loading/playing.
|
||||
this.requestSignature =
|
||||
typeof options.requestSignature === "string"
|
||||
? options.requestSignature
|
||||
@@ -389,6 +444,10 @@ class PlaybackSession extends SimpleEventEmitter {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Begins the playback flow. Returns a promise that resolves when playback
|
||||
* has either successfully started (URL or Torrent) or fatally failed.
|
||||
*/
|
||||
async start() {
|
||||
if (this.startPromise) {
|
||||
return this.startPromise;
|
||||
@@ -397,6 +456,17 @@ class PlaybackSession extends SimpleEventEmitter {
|
||||
return this.startPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* The core execution loop for the session.
|
||||
* Flow:
|
||||
* 1. Check if forced source (e.g. user manually switched to "Torrent")
|
||||
* 2. If URL available & URL-first enabled:
|
||||
* a. Probe the URL (HEAD request) to see if it's reachable.
|
||||
* b. If probe succeeds, attempt to play.
|
||||
* c. Attach watchdogs.
|
||||
* d. If watchdog triggers (stall/error), trigger `startTorrentFallback`.
|
||||
* 3. If URL fails or not available, call `startTorrentFallback`.
|
||||
*/
|
||||
async execute() {
|
||||
const {
|
||||
videoElement,
|
||||
@@ -480,6 +550,8 @@ class PlaybackSession extends SimpleEventEmitter {
|
||||
);
|
||||
}
|
||||
|
||||
// -- Fallback Logic --
|
||||
// This function switches from the "URL" path to the "Torrent" path.
|
||||
let fallbackStarted = false;
|
||||
const startTorrentFallback = async (reason) => {
|
||||
if (fallbackStarted) {
|
||||
@@ -493,6 +565,7 @@ class PlaybackSession extends SimpleEventEmitter {
|
||||
cleanupHostedUrlStatusListeners();
|
||||
cleanupDebugListeners();
|
||||
|
||||
// Reset the video element state so WebTorrent can take over clean.
|
||||
if (activeVideoEl) {
|
||||
try {
|
||||
activeVideoEl.pause();
|
||||
@@ -543,6 +616,7 @@ class PlaybackSession extends SimpleEventEmitter {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Delegate to the WebTorrent driver (see js/webtorrent.js interactions in app.js)
|
||||
const torrentInstance = await playViaWebTorrent(this.magnetForPlayback, {
|
||||
fallbackMagnet: this.fallbackMagnet,
|
||||
urlList: webSeedCandidates,
|
||||
@@ -556,6 +630,7 @@ class PlaybackSession extends SimpleEventEmitter {
|
||||
return torrentInstance;
|
||||
};
|
||||
|
||||
// -- Primary Path: Direct URL --
|
||||
if (
|
||||
this.service.urlFirstEnabled &&
|
||||
httpsUrl &&
|
||||
@@ -651,12 +726,15 @@ class PlaybackSession extends SimpleEventEmitter {
|
||||
};
|
||||
});
|
||||
|
||||
// Install the watchdogs before we assign src and play.
|
||||
const attachWatchdogs = ({ stallMs = 8000 } = {}) => {
|
||||
this.registerWatchdogs(activeVideoEl, {
|
||||
stallMs,
|
||||
onSuccess: () => outcomeResolver({ status: "success" }),
|
||||
onFallback: (reason) => {
|
||||
if (autoplayBlocked && reason === "stall") {
|
||||
// If browser blocked autoplay, a stall is expected (video is paused).
|
||||
// Don't fallback yet; wait for user interaction.
|
||||
this.service.log(
|
||||
"[playVideoWithFallback] Autoplay blocked; waiting for user gesture before falling back."
|
||||
);
|
||||
@@ -664,7 +742,7 @@ class PlaybackSession extends SimpleEventEmitter {
|
||||
message: "Press play to start the hosted video.",
|
||||
});
|
||||
this.emit("autoplay-blocked", { reason });
|
||||
attachWatchdogs({ stallMs: 0 });
|
||||
attachWatchdogs({ stallMs: 0 }); // Disable timer
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -730,6 +808,7 @@ class PlaybackSession extends SimpleEventEmitter {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
// If we are here, the watchdog triggered a fallback.
|
||||
const fallbackReason =
|
||||
playbackOutcome?.reason || "watchdog-triggered";
|
||||
const torrentInstance = await startTorrentFallback(fallbackReason);
|
||||
@@ -746,6 +825,7 @@ class PlaybackSession extends SimpleEventEmitter {
|
||||
cleanupDebugListeners();
|
||||
}
|
||||
|
||||
// -- Secondary Path: Magnet / Torrent --
|
||||
if (this.magnetForPlayback) {
|
||||
const torrentInstance = await startTorrentFallback("magnet-primary");
|
||||
this.result = torrentInstance
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
//js/webtorrent.js
|
||||
|
||||
/**
|
||||
* js/webtorrent.js
|
||||
*
|
||||
* This module wraps the WebTorrent client and manages the Service Worker integration.
|
||||
* It is responsible for the "Torrent" half of the playback strategy.
|
||||
*
|
||||
* Key Responsibilities:
|
||||
* - Singleton WebTorrent client: It ensures we reuse a single client instance to
|
||||
* avoid resource leaks and connection churn.
|
||||
* - Service Worker Registration: It registers and manages `/sw.min.js`. This is critical
|
||||
* because WebTorrent in the browser streams data via a Service Worker "proxy" that
|
||||
* intercepts HTTP requests from the <video> element and serves bytes fetched from peers.
|
||||
* - "Claim" Logic: It handles the complex dance of ensuring the Service Worker has
|
||||
* active control over the page (clients.claim) before attempting to stream, fixing
|
||||
* common "grey screen" regressions on page reload.
|
||||
* - Browser Quirks: It handles Brave/Firefox specific logic.
|
||||
*/
|
||||
|
||||
import WebTorrent from "./webtorrent.min.js";
|
||||
import { WSS_TRACKERS } from "./constants.js";
|
||||
import { safeDecodeURIComponent } from "./utils/safeDecode.js";
|
||||
@@ -151,6 +169,10 @@ export class TorrentClient {
|
||||
return this.probeClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to check if a magnet link has active peers without starting a full
|
||||
* download. Used for "Stream Health" badges/indicators in the UI.
|
||||
*/
|
||||
async probePeers(
|
||||
magnetURI,
|
||||
{ timeoutMs = 8000, maxWebConns = 2, polls = 3 } = {}
|
||||
@@ -837,6 +859,7 @@ export class TorrentClient {
|
||||
!!(initResult?.serviceWorkerReady && this.swRegistration);
|
||||
|
||||
// 2) Create the server once if not already created.
|
||||
// This "server" is the bridge that feeds the Service Worker.
|
||||
if (serviceWorkerReady && !this.serverCreated) {
|
||||
this.client.createServer({
|
||||
controller: this.swRegistration,
|
||||
|
||||
Reference in new issue
Block a user