Files
bitvid/js/utils/cardSourceVisibility.js
thePR0M3TH3ANandClaude Opus 4.8 2e510b2330 fix(liveness): don't bury WebTorrent-only content under hide-until-verified
A WebTorrent-only card's sole liveness signal is the swarm probe (20s timeout,
5min cache), and P2P liveness is slow + intermittent — a live-but-slow video is
indistinguishable from a dead one until that probe finishes. Hiding such a card
until the swarm verifies could bury perfectly good P2P content for minutes.

hide-until-verified now only suppresses a card while the FAST, reliable CDN probe
is still running. Once the CDN resolves dead (or is absent, i.e. P2P-only), it
falls back to show-pending for the WebTorrent part: shown while the swarm is
checked, hidden only once it too is confirmed dead. Burying live content is worse
than a brief flash of a dead one, and only the slow P2P probe is at issue.
cardNeedsEagerLivenessProbe likewise skips P2P-only cards (they start visible).

Tests: hide-foreign does-not-bury-WebTorrent-only coverage in
tests/video-card-source-visibility.test.mjs.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-25 09:48:55 -04:00

145 lines
4.8 KiB
JavaScript

import { getCardLivenessPolicy } from "../constants.js";
/**
* Whether a card is governed by a hide-until-verified policy (so it starts hidden
* and must be PROBED to ever appear). Such cards can't rely on the viewport
* IntersectionObserver — a `display:none` card never intersects, so the probe
* must be kicked eagerly on register. Owner's own cards are always visible and
* never gated.
*/
export function cardNeedsEagerLivenessProbe(cardLike) {
const card = resolveCardElement(cardLike);
if (!card || card.dataset.ownerIsViewer === "true") {
return false;
}
const policy = getCardLivenessPolicy();
const gated =
policy === "hide-all" ||
(policy === "hide-foreign" && card.dataset.foreign === "true");
if (!gated) {
return false;
}
// Only cards backed by a fast CDN probe are hidden-until-verified (so they need
// eager probing while hidden). P2P-only cards fall back to show-pending — they
// start visible and are probed normally on scroll, so no eager kick is needed.
return Boolean(card.dataset.urlHealthUrl);
}
function resolveCardElement(cardLike) {
if (!cardLike) {
return null;
}
const isElement =
typeof cardLike === "object" && cardLike !== null && cardLike.nodeType === 1;
if (isElement) {
const element = cardLike;
if (typeof element.closest === "function") {
if (element.classList?.contains("card")) {
return element;
}
return element.closest(".card");
}
return element.classList?.contains("card") ? element : null;
}
if (
typeof HTMLElement !== "undefined" &&
cardLike instanceof HTMLElement &&
typeof cardLike.closest === "function"
) {
if (cardLike.classList.contains("card")) {
return cardLike;
}
return cardLike.closest(".card");
}
if (typeof cardLike === "object" && cardLike !== null) {
if (typeof cardLike.card === "object" && cardLike.card?.nodeType === 1) {
return resolveCardElement(cardLike.card);
}
if (typeof cardLike.root === "object" && cardLike.root?.nodeType === 1) {
return resolveCardElement(cardLike.root);
}
}
return null;
}
export function updateVideoCardSourceVisibility(cardLike) {
const card = resolveCardElement(cardLike);
if (!card) {
return;
}
if (card.dataset.ownerIsViewer === "true") {
if (card.hidden) {
card.hidden = false;
}
if (card.dataset.sourceVisibility !== "visible") {
card.dataset.sourceVisibility = "visible";
}
return;
}
const cdnState = (card.dataset.urlHealthState || "").toLowerCase();
const streamState = (card.dataset.streamHealthState || "").toLowerCase();
const cdnPending = !cdnState || cdnState === "checking";
const streamPending = !streamState || streamState === "checking";
const cdnHealthy = cdnState === "healthy";
const streamHealthy = streamState === "healthy";
// Liveness visibility policy (config/instance-config.js → CARD_LIVENESS_POLICY):
// show-pending : show now, hide only once every source is confirmed dead.
// hide-foreign : foreign/ingested cards stay hidden until a source is proven
// playable; native cards keep show-pending.
// hide-all : every non-owner card stays hidden until proven playable.
const policy = getCardLivenessPolicy();
const isForeign = card.dataset.foreign === "true";
const hideUntilVerified =
policy === "hide-all" || (policy === "hide-foreign" && isForeign);
// Default policy: hide only once every source has come back dead.
const confirmedDead =
!cdnHealthy && !streamHealthy && !cdnPending && !streamPending;
let shouldHide;
if (!hideUntilVerified) {
shouldHide = confirmedDead;
} else if (cdnHealthy || streamHealthy) {
shouldHide = false; // proven playable → show
} else if (cdnPending) {
// Still waiting on the FAST, reliable CDN probe (~4s) — keep it hidden so a
// dead foreign card never flashes in.
shouldHide = true;
} else {
// CDN is resolved dead (or absent) and the only remaining signal is the slow,
// unreliable WebTorrent swarm probe (20s + 5min cache). Do NOT bury the card
// behind it — a live-but-slow P2P video is indistinguishable from a dead one
// until that probe finishes, and burying live content is worse than a brief
// flash. Fall back to show-pending for the stream: show while it's checked,
// hide only once it too is confirmed dead.
shouldHide = !streamHealthy && !streamPending;
}
if (shouldHide) {
if (!card.hidden) {
card.hidden = true;
}
if (card.dataset.sourceVisibility !== "hidden") {
card.dataset.sourceVisibility = "hidden";
}
return;
}
if (card.hidden) {
card.hidden = false;
}
if (card.dataset.sourceVisibility !== "visible") {
card.dataset.sourceVisibility = "visible";
}
}