mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
feat(playback): add configurable timeout for initial playback start
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).
This commit is contained in:
1 parent
e795c74a8e
commit
7921ab6a2e
4 files changed
+87
-7
No files matched your search
@@ -436,3 +436,13 @@ export const ENSURE_PRESENCE_REBROADCAST_COOLDOWN_SECONDS = 5 * 60;
|
||||
* - "torrent": Try WebTorrent first, fall back to CDN/URL.
|
||||
*/
|
||||
export const DEFAULT_PLAYBACK_SOURCE = "url";
|
||||
|
||||
/**
|
||||
* Timeout (in milliseconds) for the initial playback attempt.
|
||||
*
|
||||
* If the preferred source (URL or Torrent) does not reach the "playing" state
|
||||
* within this window, the player will automatically switch to the fallback
|
||||
* source. This prevents users from staring at a loading spinner indefinitely
|
||||
* if the primary source is slow or stalled.
|
||||
*/
|
||||
export const DEFAULT_PLAYBACK_START_TIMEOUT = 3000;
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
VIEW_COUNT_CACHE_TTL_MS,
|
||||
ENSURE_PRESENCE_REBROADCAST_COOLDOWN_SECONDS,
|
||||
DEFAULT_PLAYBACK_SOURCE,
|
||||
DEFAULT_PLAYBACK_START_TIMEOUT,
|
||||
} from "../config/instance-config.js";
|
||||
|
||||
export const WATCH_HISTORY_VERSION_TAG_VALUE = "2";
|
||||
@@ -135,3 +136,4 @@ export { VIEW_COUNT_BACKFILL_MAX_DAYS };
|
||||
export { VIEW_COUNT_CACHE_TTL_MS };
|
||||
export { ENSURE_PRESENCE_REBROADCAST_COOLDOWN_SECONDS };
|
||||
export { DEFAULT_PLAYBACK_SOURCE };
|
||||
export { DEFAULT_PLAYBACK_START_TIMEOUT };
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD as CONFIG_DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD,
|
||||
DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD as CONFIG_DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD,
|
||||
DEFAULT_PLAYBACK_SOURCE,
|
||||
DEFAULT_PLAYBACK_START_TIMEOUT,
|
||||
IS_DEV_MODE,
|
||||
} from "./config.js";
|
||||
|
||||
@@ -135,6 +136,11 @@ const SANITIZED_DEFAULT_TRUST_SEED_NPUBS = Object.freeze(
|
||||
sanitizeTrustSeedList(CONFIG_DEFAULT_TRUST_SEED_NPUBS)
|
||||
);
|
||||
|
||||
const SANITIZED_DEFAULT_PLAYBACK_START_TIMEOUT = coerceNonNegativeInteger(
|
||||
DEFAULT_PLAYBACK_START_TIMEOUT,
|
||||
3000
|
||||
);
|
||||
|
||||
export const DEFAULT_BLUR_THRESHOLD = SANITIZED_DEFAULT_BLUR_THRESHOLD;
|
||||
export const DEFAULT_AUTOPLAY_BLOCK_THRESHOLD =
|
||||
SANITIZED_DEFAULT_AUTOPLAY_BLOCK_THRESHOLD;
|
||||
@@ -143,6 +149,7 @@ export const DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD =
|
||||
export const DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD =
|
||||
SANITIZED_DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD;
|
||||
export const DEFAULT_TRUST_SEED_NPUBS = SANITIZED_DEFAULT_TRUST_SEED_NPUBS;
|
||||
export const PLAYBACK_START_TIMEOUT = SANITIZED_DEFAULT_PLAYBACK_START_TIMEOUT;
|
||||
|
||||
const DEFAULT_FLAGS = Object.freeze({
|
||||
URL_FIRST_ENABLED: DEFAULT_PLAYBACK_SOURCE !== "torrent", // try URL before magnet in the player
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { userLogger } from "../utils/logger.js";
|
||||
import { PLAYBACK_START_TIMEOUT } from "../constants.js";
|
||||
// js/services/playbackService.js
|
||||
|
||||
/**
|
||||
@@ -70,6 +71,7 @@ export class PlaybackService {
|
||||
isValidMagnetUri,
|
||||
urlFirstEnabled = true,
|
||||
analyticsCallbacks = {},
|
||||
playbackStartTimeout = PLAYBACK_START_TIMEOUT,
|
||||
} = {}) {
|
||||
if (typeof logger === "function") {
|
||||
this.logger = logger;
|
||||
@@ -84,6 +86,7 @@ export class PlaybackService {
|
||||
this.isValidMagnetUri = isValidMagnetUri;
|
||||
this.urlFirstEnabled = !!urlFirstEnabled;
|
||||
this.analyticsCallbacks = analyticsCallbacks || {};
|
||||
this.playbackStartTimeout = playbackStartTimeout;
|
||||
this.currentSession = null;
|
||||
}
|
||||
|
||||
@@ -261,7 +264,10 @@ export class PlaybackService {
|
||||
}
|
||||
|
||||
createSession(options = {}) {
|
||||
const session = new PlaybackSession(this, options);
|
||||
const session = new PlaybackSession(this, {
|
||||
...options,
|
||||
playbackStartTimeout: this.playbackStartTimeout,
|
||||
});
|
||||
this.currentSession = session;
|
||||
return session;
|
||||
}
|
||||
@@ -480,6 +486,7 @@ class PlaybackSession extends SimpleEventEmitter {
|
||||
autoplay,
|
||||
unsupportedBtihMessage = DEFAULT_UNSUPPORTED_BTITH_MESSAGE,
|
||||
forcedSource = null,
|
||||
playbackStartTimeout = PLAYBACK_START_TIMEOUT,
|
||||
} = this.options;
|
||||
|
||||
this.result = { source: null };
|
||||
@@ -724,6 +731,7 @@ class PlaybackSession extends SimpleEventEmitter {
|
||||
});
|
||||
};
|
||||
|
||||
// Use default stall timeout here; the initial start timeout is handled by the race wrapper
|
||||
attachWatchdogs({ stallMs: 8000 });
|
||||
|
||||
const handleFatalPlaybackError = (err) => {
|
||||
@@ -792,6 +800,27 @@ class PlaybackSession extends SimpleEventEmitter {
|
||||
return { status: "fallback", reason: "probe-failed" };
|
||||
};
|
||||
|
||||
// --- Timeout Wrapper ---
|
||||
const withTimeout = (promise, ms, label = "Operation") => {
|
||||
if (!ms || ms <= 0) return promise;
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
this.service.log(`[playVideoWithFallback] ${label} timed out after ${ms}ms.`);
|
||||
resolve({ status: "fallback", reason: "timeout", source: "timeout" });
|
||||
}, ms);
|
||||
|
||||
promise
|
||||
.then((result) => {
|
||||
clearTimeout(timer);
|
||||
resolve(result);
|
||||
})
|
||||
.catch((err) => {
|
||||
clearTimeout(timer);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// --- Execution Flow ---
|
||||
|
||||
let tryUrlFirst = this.service.urlFirstEnabled;
|
||||
@@ -800,28 +829,60 @@ class PlaybackSession extends SimpleEventEmitter {
|
||||
|
||||
if (tryUrlFirst) {
|
||||
if (httpsUrl) {
|
||||
const urlResult = await attemptHostedPlayback();
|
||||
// Wrap URL attempt in timeout
|
||||
const urlResult = await withTimeout(
|
||||
attemptHostedPlayback(),
|
||||
playbackStartTimeout,
|
||||
"URL Playback"
|
||||
);
|
||||
|
||||
if (urlResult && urlResult.source === "url") {
|
||||
return urlResult;
|
||||
}
|
||||
// URL Failed, try Torrent
|
||||
|
||||
// If timeout or failure, proceed to fallback
|
||||
if (urlResult?.reason === "timeout") {
|
||||
// Need to ensure URL probing/playback is effectively cancelled
|
||||
this.cleanupWatchdog();
|
||||
cleanupHostedUrlStatusListeners();
|
||||
resetVideoElement();
|
||||
}
|
||||
|
||||
const fallbackReason = urlResult?.reason || "url-unavailable";
|
||||
if (this.magnetForPlayback) {
|
||||
return await attemptTorrentPlayback(fallbackReason);
|
||||
}
|
||||
} else if (this.magnetForPlayback) {
|
||||
// No URL, try Torrent
|
||||
return await attemptTorrentPlayback("url-missing");
|
||||
}
|
||||
} else {
|
||||
// Try Torrent First
|
||||
if (this.magnetForPlayback) {
|
||||
try {
|
||||
const torrentResult = await attemptTorrentPlayback("preference");
|
||||
if (torrentResult) return torrentResult;
|
||||
// Wrap Torrent attempt in timeout
|
||||
const torrentResult = await withTimeout(
|
||||
attemptTorrentPlayback("preference"),
|
||||
playbackStartTimeout,
|
||||
"Torrent Playback"
|
||||
);
|
||||
|
||||
if (torrentResult && torrentResult.source === "torrent") {
|
||||
return torrentResult;
|
||||
}
|
||||
|
||||
if (torrentResult?.reason === "timeout") {
|
||||
this.service.log("[playVideoWithFallback] Torrent timed out; cleaning up.");
|
||||
if (
|
||||
this.service.torrentClient &&
|
||||
typeof this.service.torrentClient.cleanup === "function"
|
||||
) {
|
||||
await this.service.torrentClient.cleanup();
|
||||
}
|
||||
resetVideoElement();
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
this.service.log("[playVideoWithFallback] Torrent preference failed, trying URL:", err);
|
||||
// Clean up before trying URL
|
||||
if (
|
||||
this.service.torrentClient &&
|
||||
typeof this.service.torrentClient.cleanup === "function"
|
||||
|
||||
Reference in new issue
Block a user