mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-28 08:22:24 +00:00
Merge pull request #124 from PR0M3TH3AN/codex/improve-webtorrent-and-cdn-health-checks
Scope health probes to visible video cards
This commit is contained in:
5 files changed
+296
-54
No files matched your search
@@ -11,6 +11,7 @@ import { deriveTorrentPlaybackConfig } from "./playbackUtils.js";
|
||||
import { URL_FIRST_ENABLED } from "./constants.js";
|
||||
import { trackVideoView } from "./analytics.js";
|
||||
import { attachHealthBadges } from "./gridHealth.js";
|
||||
import { attachUrlHealthBadges } from "./urlHealthObserver.js";
|
||||
import {
|
||||
initialWhitelist,
|
||||
initialBlacklist,
|
||||
@@ -3693,9 +3694,17 @@ class bitvidApp {
|
||||
if (cardEl.dataset.urlHealthReason) {
|
||||
delete cardEl.dataset.urlHealthReason;
|
||||
}
|
||||
cardEl.dataset.urlHealthEventId = video.id || "";
|
||||
cardEl.dataset.urlHealthUrl = encodeURIComponent(trimmedUrl);
|
||||
} else {
|
||||
cardEl.dataset.urlHealthState = "offline";
|
||||
cardEl.dataset.urlHealthReason = "missing-source";
|
||||
if (cardEl.dataset.urlHealthEventId) {
|
||||
delete cardEl.dataset.urlHealthEventId;
|
||||
}
|
||||
if (cardEl.dataset.urlHealthUrl) {
|
||||
delete cardEl.dataset.urlHealthUrl;
|
||||
}
|
||||
}
|
||||
if (magnetProvided && magnetSupported) {
|
||||
cardEl.dataset.streamHealthState = "checking";
|
||||
@@ -3832,11 +3841,18 @@ class bitvidApp {
|
||||
if (trimmedUrl) {
|
||||
const badgeEl = cardEl.querySelector("[data-url-health-state]");
|
||||
if (badgeEl) {
|
||||
this.handleUrlHealthBadge({
|
||||
video,
|
||||
url: trimmedUrl,
|
||||
badgeEl,
|
||||
});
|
||||
badgeEl.dataset.urlHealthEventId = video.id || "";
|
||||
badgeEl.dataset.urlHealthUrl = encodeURIComponent(trimmedUrl);
|
||||
}
|
||||
} else {
|
||||
const badgeEl = cardEl.querySelector("[data-url-health-state]");
|
||||
if (badgeEl) {
|
||||
if (badgeEl.dataset.urlHealthEventId) {
|
||||
delete badgeEl.dataset.urlHealthEventId;
|
||||
}
|
||||
if (badgeEl.dataset.urlHealthUrl) {
|
||||
delete badgeEl.dataset.urlHealthUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3852,6 +3868,10 @@ class bitvidApp {
|
||||
this.videoList.innerHTML = "";
|
||||
this.videoList.appendChild(fragment);
|
||||
attachHealthBadges(this.videoList);
|
||||
attachUrlHealthBadges(this.videoList, ({ badgeEl, url, eventId }) => {
|
||||
const video = this.videosMap.get(eventId) || { id: eventId };
|
||||
this.handleUrlHealthBadge({ video, url, badgeEl });
|
||||
});
|
||||
|
||||
// Ensure every thumbnail can recover with a fallback image if the primary
|
||||
// source fails to load or returns a zero-sized response (some CDNs error
|
||||
|
||||
+25
-8
@@ -7,6 +7,7 @@ import {
|
||||
import { app } from "./app.js";
|
||||
import { subscriptions } from "./subscriptions.js"; // <-- NEW import
|
||||
import { attachHealthBadges } from "./gridHealth.js";
|
||||
import { attachUrlHealthBadges } from "./urlHealthObserver.js";
|
||||
import { initialBlacklist, initialWhitelist } from "./lists.js";
|
||||
import { isWhitelistEnabled } from "./config.js";
|
||||
|
||||
@@ -445,9 +446,17 @@ async function loadUserVideos(pubkey) {
|
||||
if (cardEl.dataset.urlHealthReason) {
|
||||
delete cardEl.dataset.urlHealthReason;
|
||||
}
|
||||
cardEl.dataset.urlHealthEventId = video.id || "";
|
||||
cardEl.dataset.urlHealthUrl = encodeURIComponent(trimmedUrl);
|
||||
} else {
|
||||
cardEl.dataset.urlHealthState = "offline";
|
||||
cardEl.dataset.urlHealthReason = "missing-source";
|
||||
if (cardEl.dataset.urlHealthEventId) {
|
||||
delete cardEl.dataset.urlHealthEventId;
|
||||
}
|
||||
if (cardEl.dataset.urlHealthUrl) {
|
||||
delete cardEl.dataset.urlHealthUrl;
|
||||
}
|
||||
}
|
||||
if (magnetProvided && magnetSupported) {
|
||||
cardEl.dataset.streamHealthState = "checking";
|
||||
@@ -491,14 +500,18 @@ async function loadUserVideos(pubkey) {
|
||||
}
|
||||
});
|
||||
|
||||
if (trimmedUrl) {
|
||||
const badgeEl = cardEl.querySelector("[data-url-health-state]");
|
||||
if (badgeEl) {
|
||||
app.handleUrlHealthBadge({
|
||||
video,
|
||||
url: trimmedUrl,
|
||||
badgeEl,
|
||||
});
|
||||
const badgeEl = cardEl.querySelector("[data-url-health-state]");
|
||||
if (badgeEl) {
|
||||
if (trimmedUrl) {
|
||||
badgeEl.dataset.urlHealthEventId = video.id || "";
|
||||
badgeEl.dataset.urlHealthUrl = encodeURIComponent(trimmedUrl);
|
||||
} else {
|
||||
if (badgeEl.dataset.urlHealthEventId) {
|
||||
delete badgeEl.dataset.urlHealthEventId;
|
||||
}
|
||||
if (badgeEl.dataset.urlHealthUrl) {
|
||||
delete badgeEl.dataset.urlHealthUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -508,6 +521,10 @@ async function loadUserVideos(pubkey) {
|
||||
container.appendChild(fragment);
|
||||
|
||||
attachHealthBadges(container);
|
||||
attachUrlHealthBadges(container, ({ badgeEl, url, eventId }) => {
|
||||
const video = app.videosMap.get(eventId) || { id: eventId };
|
||||
app.handleUrlHealthBadge({ video, url, badgeEl });
|
||||
});
|
||||
|
||||
window.app.videoList = container;
|
||||
window.app.attachVideoListHandler();
|
||||
|
||||
+93
-33
@@ -165,8 +165,9 @@ function queueProbe(magnet, cacheKey, priority = 0) {
|
||||
|
||||
const PROBE_CACHE_TTL_MS = 5 * 60 * 1000;
|
||||
const PROBE_TIMEOUT_MS = 8000;
|
||||
const PROBE_CONCURRENCY = 6;
|
||||
const PROBE_CONCURRENCY = 12;
|
||||
const PROBE_POLL_COUNT = 3;
|
||||
const PRIORITY_BASELINE = 1_000_000;
|
||||
|
||||
class ProbeQueue {
|
||||
constructor(max = 2) {
|
||||
@@ -285,48 +286,107 @@ function getViewportCenter() {
|
||||
};
|
||||
}
|
||||
|
||||
function calculateDistanceSquared(entry, viewportCenter) {
|
||||
if (!viewportCenter) {
|
||||
return Number.POSITIVE_INFINITY;
|
||||
}
|
||||
const rect = getIntersectionRect(entry);
|
||||
if (!rect) {
|
||||
return Number.POSITIVE_INFINITY;
|
||||
}
|
||||
const centerX = rect.left + rect.width / 2;
|
||||
const centerY = rect.top + rect.height / 2;
|
||||
const dx = centerX - viewportCenter.x;
|
||||
const dy = centerY - viewportCenter.y;
|
||||
return dx * dx + dy * dy;
|
||||
}
|
||||
|
||||
function computeVisibilityPriority({ ratio, distance }) {
|
||||
const normalizedRatio = Number.isFinite(ratio)
|
||||
? Math.min(Math.max(ratio, 0), 1)
|
||||
: 0;
|
||||
const distanceScore = Number.isFinite(distance) ? 1 / (1 + distance) : 0;
|
||||
return normalizedRatio * 1000 + distanceScore;
|
||||
}
|
||||
|
||||
function prioritizeEntries(entries, viewportCenter) {
|
||||
if (!Array.isArray(entries) || entries.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return entries
|
||||
const filtered = entries
|
||||
.filter((entry) => entry.isIntersecting && entry.target instanceof HTMLElement)
|
||||
.map((entry) => {
|
||||
const rect = getIntersectionRect(entry);
|
||||
if (!rect) {
|
||||
return null;
|
||||
}
|
||||
const ratio =
|
||||
typeof entry.intersectionRatio === "number" ? entry.intersectionRatio : 0;
|
||||
const distance = calculateDistanceSquared(entry, viewportCenter);
|
||||
const priority = computeVisibilityPriority({ ratio, distance });
|
||||
return { entry, ratio, distance, priority };
|
||||
const centerY = rect.top + rect.height / 2;
|
||||
const verticalDistance = viewportCenter
|
||||
? Math.abs(centerY - viewportCenter.y)
|
||||
: Number.POSITIVE_INFINITY;
|
||||
return { entry, ratio, centerY, verticalDistance };
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (b.priority !== a.priority) {
|
||||
return b.priority - a.priority;
|
||||
.filter(Boolean);
|
||||
|
||||
if (filtered.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!viewportCenter) {
|
||||
return filtered
|
||||
.sort((a, b) => {
|
||||
if (b.ratio !== a.ratio) {
|
||||
return b.ratio - a.ratio;
|
||||
}
|
||||
return a.centerY - b.centerY;
|
||||
})
|
||||
.map((item, index) => ({
|
||||
entry: item.entry,
|
||||
priority: PRIORITY_BASELINE - index,
|
||||
}));
|
||||
}
|
||||
|
||||
const ordered = filtered
|
||||
.slice()
|
||||
.sort((a, b) => a.centerY - b.centerY);
|
||||
|
||||
let centerIndex = 0;
|
||||
let minDistance = Number.POSITIVE_INFINITY;
|
||||
for (let i = 0; i < ordered.length; i += 1) {
|
||||
const candidate = ordered[i];
|
||||
if (candidate.verticalDistance < minDistance) {
|
||||
minDistance = candidate.verticalDistance;
|
||||
centerIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
const prioritized = [];
|
||||
const pushCandidate = (candidate) => {
|
||||
if (!candidate) {
|
||||
return;
|
||||
}
|
||||
prioritized.push(candidate);
|
||||
};
|
||||
|
||||
pushCandidate(ordered[centerIndex]);
|
||||
|
||||
let left = centerIndex - 1;
|
||||
let right = centerIndex + 1;
|
||||
while (left >= 0 || right < ordered.length) {
|
||||
const leftCandidate = left >= 0 ? ordered[left] : null;
|
||||
const rightCandidate = right < ordered.length ? ordered[right] : null;
|
||||
|
||||
if (leftCandidate && rightCandidate) {
|
||||
const leftDistance = leftCandidate.verticalDistance;
|
||||
const rightDistance = rightCandidate.verticalDistance;
|
||||
const distanceDelta = Math.abs(leftDistance - rightDistance);
|
||||
if (distanceDelta <= 0.5) {
|
||||
if (rightCandidate.ratio > leftCandidate.ratio) {
|
||||
pushCandidate(rightCandidate);
|
||||
right += 1;
|
||||
} else {
|
||||
pushCandidate(leftCandidate);
|
||||
left -= 1;
|
||||
}
|
||||
} else if (leftDistance < rightDistance) {
|
||||
pushCandidate(leftCandidate);
|
||||
left -= 1;
|
||||
} else {
|
||||
pushCandidate(rightCandidate);
|
||||
right += 1;
|
||||
}
|
||||
return a.distance - b.distance;
|
||||
});
|
||||
} else if (rightCandidate) {
|
||||
pushCandidate(rightCandidate);
|
||||
right += 1;
|
||||
} else if (leftCandidate) {
|
||||
pushCandidate(leftCandidate);
|
||||
left -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
return prioritized.map((candidate, index) => ({
|
||||
entry: candidate.entry,
|
||||
priority: PRIORITY_BASELINE - index,
|
||||
}));
|
||||
}
|
||||
|
||||
function processObserverEntries(entries, state) {
|
||||
|
||||
+29
-8
@@ -4,6 +4,7 @@ import {
|
||||
convertEventToVideo as sharedConvertEventToVideo,
|
||||
} from "./nostr.js";
|
||||
import { attachHealthBadges } from "./gridHealth.js";
|
||||
import { attachUrlHealthBadges } from "./urlHealthObserver.js";
|
||||
|
||||
function getAbsoluteShareUrl(nevent) {
|
||||
if (!nevent) {
|
||||
@@ -479,9 +480,17 @@ class SubscriptionsManager {
|
||||
if (cardEl.dataset.urlHealthReason) {
|
||||
delete cardEl.dataset.urlHealthReason;
|
||||
}
|
||||
cardEl.dataset.urlHealthEventId = video.id || "";
|
||||
cardEl.dataset.urlHealthUrl = encodeURIComponent(trimmedUrl);
|
||||
} else {
|
||||
cardEl.dataset.urlHealthState = "offline";
|
||||
cardEl.dataset.urlHealthReason = "missing-source";
|
||||
if (cardEl.dataset.urlHealthEventId) {
|
||||
delete cardEl.dataset.urlHealthEventId;
|
||||
}
|
||||
if (cardEl.dataset.urlHealthUrl) {
|
||||
delete cardEl.dataset.urlHealthUrl;
|
||||
}
|
||||
}
|
||||
if (magnetProvided && magnetSupported) {
|
||||
cardEl.dataset.streamHealthState = "checking";
|
||||
@@ -519,14 +528,18 @@ class SubscriptionsManager {
|
||||
delete cardEl.dataset.magnet;
|
||||
}
|
||||
|
||||
if (trimmedUrl && window.app?.handleUrlHealthBadge) {
|
||||
const badgeEl = cardEl.querySelector("[data-url-health-state]");
|
||||
if (badgeEl) {
|
||||
window.app.handleUrlHealthBadge({
|
||||
video,
|
||||
url: trimmedUrl,
|
||||
badgeEl,
|
||||
});
|
||||
const badgeEl = cardEl.querySelector("[data-url-health-state]");
|
||||
if (badgeEl) {
|
||||
if (trimmedUrl) {
|
||||
badgeEl.dataset.urlHealthEventId = video.id || "";
|
||||
badgeEl.dataset.urlHealthUrl = encodeURIComponent(trimmedUrl);
|
||||
} else {
|
||||
if (badgeEl.dataset.urlHealthEventId) {
|
||||
delete badgeEl.dataset.urlHealthEventId;
|
||||
}
|
||||
if (badgeEl.dataset.urlHealthUrl) {
|
||||
delete badgeEl.dataset.urlHealthUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -535,6 +548,14 @@ class SubscriptionsManager {
|
||||
|
||||
container.appendChild(fragment);
|
||||
attachHealthBadges(container);
|
||||
attachUrlHealthBadges(container, ({ badgeEl, url, eventId }) => {
|
||||
if (!window.app?.handleUrlHealthBadge) {
|
||||
return;
|
||||
}
|
||||
const video =
|
||||
window.app.videosMap?.get?.(eventId) || { id: eventId };
|
||||
window.app.handleUrlHealthBadge({ video, url, badgeEl });
|
||||
});
|
||||
|
||||
if (window.app) {
|
||||
window.app.videoList = container;
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
const containerState = new WeakMap();
|
||||
const ROOT_MARGIN = "0px";
|
||||
const THRESHOLD = 0.25;
|
||||
|
||||
function decodeUrl(value) {
|
||||
if (typeof value !== "string" || !value) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
return decodeURIComponent(value);
|
||||
} catch (err) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
function ensureState(container) {
|
||||
let state = containerState.get(container);
|
||||
if (state) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const observedCards = new WeakSet();
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
processEntries(entries, state);
|
||||
},
|
||||
{ root: null, rootMargin: ROOT_MARGIN, threshold: THRESHOLD }
|
||||
);
|
||||
|
||||
state = {
|
||||
observer,
|
||||
observedCards,
|
||||
onCheck: null,
|
||||
};
|
||||
containerState.set(container, state);
|
||||
return state;
|
||||
}
|
||||
|
||||
function processEntries(entries, state) {
|
||||
if (!state?.onCheck || !Array.isArray(entries) || !entries.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
entries.forEach((entry) => {
|
||||
if (!entry.isIntersecting || entry.intersectionRatio <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const card = entry.target;
|
||||
if (!(card instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const badgeEl = card.querySelector("[data-url-health-state]");
|
||||
if (!(badgeEl instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const encodedUrl =
|
||||
card.dataset.urlHealthUrl || badgeEl.dataset.urlHealthUrl || "";
|
||||
const eventId =
|
||||
card.dataset.urlHealthEventId || badgeEl.dataset.urlHealthEventId || "";
|
||||
|
||||
if (!encodedUrl || !eventId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentState =
|
||||
card.dataset.urlHealthState || badgeEl.dataset.urlHealthState;
|
||||
if (currentState && currentState !== "checking") {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = decodeUrl(encodedUrl);
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
state.onCheck({ card, badgeEl, url, eventId });
|
||||
} catch (err) {
|
||||
console.warn("[urlHealthObserver] onCheck handler failed", err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function attachUrlHealthBadges(container, onCheck) {
|
||||
if (!(container instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const state = ensureState(container);
|
||||
if (typeof onCheck === "function") {
|
||||
state.onCheck = onCheck;
|
||||
}
|
||||
|
||||
const cards = container.querySelectorAll(".video-card");
|
||||
cards.forEach((card) => {
|
||||
if (!(card instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
if (state.observedCards.has(card)) {
|
||||
return;
|
||||
}
|
||||
state.observedCards.add(card);
|
||||
state.observer.observe(card);
|
||||
});
|
||||
|
||||
const records = state.observer.takeRecords();
|
||||
if (records.length) {
|
||||
processEntries(records, state);
|
||||
}
|
||||
}
|
||||
|
||||
export function refreshUrlHealthBadges(container) {
|
||||
const state = containerState.get(container);
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
const records = state.observer.takeRecords();
|
||||
if (records.length) {
|
||||
processEntries(records, state);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user