From eb9b50b572fa8921863a20bdabafba3f549587c2 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 24 Sep 2025 14:57:11 -0400 Subject: [PATCH] fix: ensure modal poster clears when playback starts --- js/app.js | 80 +++++++++++++++++++++++++++++++++---- js/constants.js | 7 ++++ js/magnetUtils.js | 10 ++--- tests/magnet-utils.test.mjs | 9 ++--- 4 files changed, 87 insertions(+), 19 deletions(-) create mode 100644 js/constants.js diff --git a/js/app.js b/js/app.js index 73bbd686..d765d784 100644 --- a/js/app.js +++ b/js/app.js @@ -24,6 +24,11 @@ const UNSUPPORTED_BTITH_MESSAGE = "This magnet link is missing a compatible BitTorrent v1 info hash."; const FALLBACK_THUMBNAIL_SRC = "assets/jpg/video-thumbnail-fallback.jpg"; +// NOTE: The modal uses a "please stand by" animated poster while the torrent +// or direct URL boots up. We've regressed multiple times by forgetting to clear +// that poster once playback starts, which leaves the loading GIF covering the +// video even though audio is playing. Centralising the cleanup logic makes it +// easier to spot those regressions; see forceRemoveModalPoster() below. const MODAL_LOADING_POSTER = "assets/gif/please-stand-by.gif"; /** @@ -595,13 +600,10 @@ class bitvidApp { const videoEl = this.modalVideo; const clearPoster = () => { - videoEl.removeEventListener("loadeddata", clearPoster); - videoEl.removeEventListener("playing", clearPoster); - this.modalPosterCleanup = null; - videoEl.poster = ""; - if (videoEl.hasAttribute("poster")) { - videoEl.removeAttribute("poster"); - } + // Delegate to the shared helper so every code path clears the GIF in the + // same way. If we ever change how the poster works we only need to update + // forceRemoveModalPoster(). + this.forceRemoveModalPoster("playback-event"); }; videoEl.addEventListener("loadeddata", clearPoster); @@ -615,6 +617,52 @@ class bitvidApp { }; } + /** + * Forcefully strip the modal's loading poster. + * + * The video modal shows an animated "please stand by" GIF while we wait for a + * `loadeddata` or `playing` event. Historically, small refactors would remove + * the matching cleanup path which left the GIF covering real playback. By + * routing every caller through this helper we can add defensive clears (for + * example when WebTorrent reports progress) without duplicating poster logic + * all over the file. + * + * @param {string} reason A debugging hint that documents why the poster was + * cleared. + * @returns {boolean} `true` if a poster attribute/value was removed. + */ + forceRemoveModalPoster(reason = "manual-clear") { + if (!this.modalVideo) { + return false; + } + + const videoEl = this.modalVideo; + + if (typeof this.modalPosterCleanup === "function") { + this.modalPosterCleanup(); + this.modalPosterCleanup = null; + } + + const hadPoster = + videoEl.hasAttribute("poster") || + (typeof videoEl.poster === "string" && videoEl.poster !== ""); + + if (!hadPoster) { + return false; + } + + videoEl.poster = ""; + if (videoEl.hasAttribute("poster")) { + videoEl.removeAttribute("poster"); + } + + console.debug( + `[bitvidApp] Cleared modal loading poster (${reason}).` + ); + + return true; + } + /** * Initialize the upload modal (upload-modal.html). */ @@ -1799,6 +1847,15 @@ class bitvidApp { return; } + if (torrent.ready || (typeof torrent.progress === "number" && torrent.progress > 0)) { + // Belt-and-suspenders: if WebTorrent reports progress but the DOM events + // failed to fire we still rip off the loading GIF. This regression has + // bitten us in past releases, so the extra clear is intentional. + this.forceRemoveModalPoster( + torrent.ready ? "torrent-ready-flag" : "torrent-progress" + ); + } + // Log only fields that actually exist on the torrent: console.log("[DEBUG] torrent.progress =", torrent.progress); console.log("[DEBUG] torrent.numPeers =", torrent.numPeers); @@ -2139,6 +2196,9 @@ class bitvidApp { if (this.modalStatus) { this.modalStatus.textContent = "Streaming from URL"; } + // Defensive clear so the loading GIF never lingers if browsers skip + // firing `playing` (some autoplay policies do this). + this.forceRemoveModalPoster("http-loadeddata"); }; this.modalVideo.addEventListener("loadeddata", markUrlReady, { @@ -2216,6 +2276,12 @@ class bitvidApp { cacheBustedMagnet, this.modalVideo ); + if (torrentInstance && torrentInstance.ready) { + // Some browsers delay `playing` events for MediaSource-backed torrents. + // Clearing the poster here prevents the historic "GIF stuck over the + // video" regression when WebTorrent is already feeding data. + this.forceRemoveModalPoster("webtorrent-ready"); + } this.startTorrentStatusMirrors(torrentInstance); return torrentInstance; }; diff --git a/js/constants.js b/js/constants.js new file mode 100644 index 00000000..8efdc688 --- /dev/null +++ b/js/constants.js @@ -0,0 +1,7 @@ +export const URL_FIRST_ENABLED = true; +export const ACCEPT_LEGACY_V1 = true; +export const WSS_TRACKERS = [ + "wss://tracker.openwebtorrent.com", + "wss://tracker.btorrent.xyz", + "wss://tracker.webtorrent.dev", +]; diff --git a/js/magnetUtils.js b/js/magnetUtils.js index 4cbcb737..1bc0b54f 100644 --- a/js/magnetUtils.js +++ b/js/magnetUtils.js @@ -1,10 +1,8 @@ // js/magnetUtils.js -export const DEFAULT_WSS_TRACKERS = [ - "wss://tracker.openwebtorrent.com", - "wss://tracker.btorrent.xyz", - "wss://tracker.fastcast.nz", -]; +import { WSS_TRACKERS } from "./constants.js"; + +export { WSS_TRACKERS }; const HEX_INFO_HASH = /^[0-9a-f]{40}$/i; const BTIH_PREFIX = "urn:btih:"; @@ -200,7 +198,7 @@ export function normalizeAndAugmentMagnet( ); const trackerCandidates = [ - ...DEFAULT_WSS_TRACKERS, + ...WSS_TRACKERS, ...extraTrackers, ]; diff --git a/tests/magnet-utils.test.mjs b/tests/magnet-utils.test.mjs index 601ac14e..ac313f47 100644 --- a/tests/magnet-utils.test.mjs +++ b/tests/magnet-utils.test.mjs @@ -1,9 +1,6 @@ import assert from "node:assert/strict"; -import { - DEFAULT_WSS_TRACKERS, - normalizeAndAugmentMagnet, - safeDecodeMagnet, -} from "../js/magnetUtils.js"; +import { WSS_TRACKERS } from "../js/constants.js"; +import { normalizeAndAugmentMagnet, safeDecodeMagnet } from "../js/magnetUtils.js"; function getParamValues(magnet, key) { const parsed = new URL(magnet); @@ -17,7 +14,7 @@ function getParamValues(magnet, key) { const xtValues = getParamValues(result.magnet, "xt"); assert.deepEqual(xtValues, [`urn:btih:${infoHash}`]); const trackerValues = getParamValues(result.magnet, "tr"); - for (const tracker of DEFAULT_WSS_TRACKERS) { + for (const tracker of WSS_TRACKERS) { assert.ok( trackerValues.includes(tracker), `Expected tracker ${tracker} to be appended`