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:
google-labs-jules[bot] committed 2026-01-26 15:55:11 +00:00
1 parent e795c74a8e
commit 7921ab6a2e
4 files changed
+87 -7

No files matched your search

+10
View File
@@ -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;
+2
View File
@@ -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 };
+7
View File
@@ -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
+68 -7
View File
@@ -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"