mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 16:02:26 +00:00
bitvid attached every hosted source with `videoEl.src = url`, which only plays
HLS on Safari-family browsers. Firefox 146 fails with MEDIA_ERR_SRC_NOT_SUPPORTED
(canPlayType returns ""), so every .m3u8 note -- e.g. anything published by
nostube/slidestr -- was unplayable there and the URL-health badge showed
"offline". Chrome/Brave 149 happen to play HLS natively, which is why this
looked browser-specific.
Vendor hls.js (npm run build:hls) and route HLS sources through it:
- js/services/hlsPlayback.js owns detection, engine choice and the hls.js
lifecycle. Engine choice keys off MediaSource, NOT canPlayType: Chrome
returns "maybe" for every HLS type, so a native-first branch would hang.
Native is used only when MSE is genuinely unavailable.
- The bundle is the FULL build, not hls.js/light -- light drops the alternate
audio stream controller and real ladders demux audio into an EXT-X-MEDIA
group. It is lazy-imported only when a .m3u8 is encountered, so ordinary
MP4/WebTorrent playback never pays the ~500KB.
- modalCoordinator tears the instance down first; clearing `src` alone leaves
the transmuxing worker and segment fetches running after the modal closes.
- urlHealthController probes HLS by fetching the playlist for #EXTM3U. The old
bare-<video> probe could not load it and on Chrome emitted no events at all,
burning the full timeout on every HLS card.
- HLS_PLAYBACK_START_TIMEOUT widens the 3s start window, which fired
mid-startup: the bundle, master playlist, variant playlist, init segment and
first media segment are four-plus serial round trips.
Also fixes two bugs found while verifying this:
- A stale MediaError was reported as the current failure. Teardown clears `src`
and calls load(), leaving MEDIA_ERR_SRC_NOT_SUPPORTED ("Empty src attribute")
on the element; a later watchdog fallback rendered that as "Hosted playback
failed: source not supported or blocked." over a stream that was playing
fine. Only a MediaError produced by the current attempt is surfaced now.
Sources are also bound before the watchdogs are armed, so hls.js's own
attachMedia() src swap cannot read as a playback failure.
- `audio/x-mpegurl` is a legacy alias for an HLS playlist of any kind, not an
audio-only assertion. isAudioOnlyVideoObject misfiled HLS video ladders
published with it as podcasts and dropped them from every video feed.
Verified end-to-end in Chromium, Chrome 149, Brave 149 and Firefox 146 against
a live HLS stream: Firefox goes from "No playable source found." to playing.
Rollback: stop calling attachHlsSource() and the previous `src` assignment
returns; see docs/playback-fallback.md.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
687 lines
22 KiB
JavaScript
687 lines
22 KiB
JavaScript
import {
|
|
DEFAULT_AUTOPLAY_BLOCK_THRESHOLD as CONFIG_DEFAULT_AUTOPLAY_BLOCK_THRESHOLD,
|
|
DEFAULT_BLUR_THRESHOLD as CONFIG_DEFAULT_BLUR_THRESHOLD,
|
|
DEFAULT_TRUST_SEED_NPUBS as CONFIG_DEFAULT_TRUST_SEED_NPUBS,
|
|
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,
|
|
FEATURE_NIP71_INGEST as CONFIG_FEATURE_NIP71_INGEST,
|
|
FEATURE_AUDIO_INGEST as CONFIG_FEATURE_AUDIO_INGEST,
|
|
FEATURE_BITCOIN_CONNECT as CONFIG_FEATURE_BITCOIN_CONNECT,
|
|
FEATURE_BITLOGIN as CONFIG_FEATURE_BITLOGIN,
|
|
FEATURE_BLOSSOM_STORAGE as CONFIG_FEATURE_BLOSSOM_STORAGE,
|
|
FEATURE_BLOSSOM_TORRENT_METADATA as CONFIG_FEATURE_BLOSSOM_TORRENT_METADATA,
|
|
CARD_LIVENESS_POLICY as CONFIG_CARD_LIVENESS_POLICY,
|
|
LIVENESS_PROBE_PREFETCH_MARGIN as CONFIG_LIVENESS_PROBE_PREFETCH_MARGIN,
|
|
IS_DEV_MODE,
|
|
} from "./config.js";
|
|
|
|
// Time constants in milliseconds
|
|
export const ONE_SECOND_MS = 1000;
|
|
export const ONE_MINUTE_MS = 60 * ONE_SECOND_MS;
|
|
export const FIVE_MINUTES_MS = 5 * ONE_MINUTE_MS;
|
|
export const TEN_MINUTES_MS = 10 * ONE_MINUTE_MS;
|
|
export const FORTY_FIVE_MINUTES_MS = 45 * ONE_MINUTE_MS;
|
|
export const ONE_HOUR_MS = 60 * ONE_MINUTE_MS;
|
|
export const ONE_DAY_MS = 24 * ONE_HOUR_MS;
|
|
|
|
// Specific timeouts and TTLs
|
|
export const SHORT_TIMEOUT_MS = 5000;
|
|
export const STANDARD_TIMEOUT_MS = 10000;
|
|
export const MEDIUM_TIMEOUT_MS = 30000;
|
|
export const LONG_TIMEOUT_MS = 60000;
|
|
|
|
// Limits
|
|
export const MAX_BLOCKLIST_ENTRIES = 5000;
|
|
|
|
export const UI_FEEDBACK_DELAY_MS = 2000;
|
|
export const DEBOUNCE_DELAY_MS = 2000;
|
|
export const NETWORK_RETRY_DELAY_MS = 1500;
|
|
|
|
export const DEFAULT_CACHE_TTL_MS = FIVE_MINUTES_MS;
|
|
export const PROFILE_CACHE_TTL_MS = TEN_MINUTES_MS;
|
|
export const URL_HEALTH_TTL_MS = FORTY_FIVE_MINUTES_MS;
|
|
export const URL_HEALTH_RETRY_MS = FIVE_MINUTES_MS;
|
|
// Per-source playability probe budget. Kept tight so a dead host yields a fast
|
|
// verdict (and the card hides quickly) — a slow-but-valid CDN still gets a retry
|
|
// via URL_HEALTH/RETRY paths. Lowered from 8s.
|
|
export const URL_PROBE_TIMEOUT_MS = 4 * ONE_SECOND_MS;
|
|
|
|
function coerceNonNegativeInteger(value, fallback) {
|
|
const numeric = Number(value);
|
|
if (!Number.isFinite(numeric)) {
|
|
return fallback;
|
|
}
|
|
|
|
const sanitized = Math.max(0, Math.floor(numeric));
|
|
if (!Number.isFinite(sanitized)) {
|
|
return fallback;
|
|
}
|
|
|
|
return sanitized;
|
|
}
|
|
|
|
function coerceBoolean(value, fallback) {
|
|
if (typeof value === "boolean") {
|
|
return value;
|
|
}
|
|
if (typeof value === "string") {
|
|
const normalized = value.trim().toLowerCase();
|
|
if (!normalized) {
|
|
return fallback;
|
|
}
|
|
if (["false", "0", "off", "no"].includes(normalized)) {
|
|
return false;
|
|
}
|
|
if (["true", "1", "on", "yes"].includes(normalized)) {
|
|
return true;
|
|
}
|
|
}
|
|
if (value == null) {
|
|
return fallback;
|
|
}
|
|
return Boolean(value);
|
|
}
|
|
|
|
function freezeTrackers(list) {
|
|
return Object.freeze([...list]);
|
|
}
|
|
|
|
function sanitizeTrackerList(candidate, fallbackTrackers) {
|
|
const fallback = Array.isArray(fallbackTrackers) ? fallbackTrackers : [];
|
|
const input = Array.isArray(candidate) ? candidate : fallback;
|
|
const seen = new Set();
|
|
const sanitized = [];
|
|
|
|
for (const tracker of input) {
|
|
if (typeof tracker !== "string") {
|
|
continue;
|
|
}
|
|
const trimmed = tracker.trim();
|
|
if (!trimmed) {
|
|
continue;
|
|
}
|
|
if (!/^wss:\/\//i.test(trimmed)) {
|
|
continue;
|
|
}
|
|
const normalized = trimmed.toLowerCase();
|
|
if (seen.has(normalized)) {
|
|
continue;
|
|
}
|
|
seen.add(normalized);
|
|
sanitized.push(trimmed);
|
|
}
|
|
|
|
if (!sanitized.length) {
|
|
return [...fallback];
|
|
}
|
|
|
|
return sanitized;
|
|
}
|
|
|
|
function sanitizeTrustSeedList(candidate) {
|
|
if (!Array.isArray(candidate)) {
|
|
return [];
|
|
}
|
|
|
|
const seen = new Set();
|
|
const sanitized = [];
|
|
|
|
for (const npub of candidate) {
|
|
if (typeof npub !== "string") {
|
|
continue;
|
|
}
|
|
const trimmed = npub.trim();
|
|
if (!trimmed) {
|
|
continue;
|
|
}
|
|
if (seen.has(trimmed)) {
|
|
continue;
|
|
}
|
|
seen.add(trimmed);
|
|
sanitized.push(trimmed);
|
|
}
|
|
|
|
return sanitized;
|
|
}
|
|
|
|
const DEFAULT_WSS_TRACKERS = Object.freeze([
|
|
"wss://tracker.openwebtorrent.com",
|
|
"wss://tracker.files.fm:7073/announce",
|
|
"wss://tracker.novage.com.ua:443/announce",
|
|
"wss://tracker.webtorrent.dev",
|
|
]);
|
|
|
|
const SANITIZED_DEFAULT_BLUR_THRESHOLD = coerceNonNegativeInteger(
|
|
CONFIG_DEFAULT_BLUR_THRESHOLD,
|
|
1,
|
|
);
|
|
const SANITIZED_DEFAULT_AUTOPLAY_BLOCK_THRESHOLD = coerceNonNegativeInteger(
|
|
CONFIG_DEFAULT_AUTOPLAY_BLOCK_THRESHOLD,
|
|
1,
|
|
);
|
|
const SANITIZED_DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD = coerceNonNegativeInteger(
|
|
CONFIG_DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD,
|
|
4,
|
|
);
|
|
const SANITIZED_DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD = coerceNonNegativeInteger(
|
|
CONFIG_DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD,
|
|
3,
|
|
);
|
|
|
|
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;
|
|
export const DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD =
|
|
SANITIZED_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;
|
|
|
|
// HLS needs a bigger start window than a progressive MP4. Before the first
|
|
// frame can render, hls.js must lazy-load its (vendored) bundle, fetch the
|
|
// master playlist, fetch the chosen variant playlist, then fetch the init
|
|
// segment and the first media segment — four or more serial round trips versus
|
|
// the single ranged GET a plain MP4 needs. The 3s default fires mid-startup and
|
|
// reports a perfectly healthy stream as "No playable source found."
|
|
export const HLS_PLAYBACK_START_TIMEOUT = Math.max(
|
|
SANITIZED_DEFAULT_PLAYBACK_START_TIMEOUT,
|
|
15000
|
|
);
|
|
|
|
const DEFAULT_FLAGS = Object.freeze({
|
|
URL_FIRST_ENABLED: DEFAULT_PLAYBACK_SOURCE !== "torrent", // try URL before magnet in the player
|
|
FEATURE_WATCH_HISTORY_V2: true,
|
|
FEATURE_PUBLISH_NIP71: false,
|
|
// Opt-in NIP-71 *mirror* (addressable 34235/36) managed per-video from the
|
|
// My Videos tab. Independent of FEATURE_PUBLISH_NIP71 (legacy 21/22 auto-publish).
|
|
FEATURE_NIP71_MIRROR: true,
|
|
// Inbound ingest of NIP-71 videos published by other Nostr apps. Sourced from
|
|
// the admin instance config so a deployer flips it in one place.
|
|
FEATURE_NIP71_INGEST: CONFIG_FEATURE_NIP71_INGEST !== false,
|
|
// Audio / Music / Podcast surfaces (docs/audio-integration-plan.md, TODO #60).
|
|
// Default OFF ("off = no trace") — only an explicit `true` in the instance
|
|
// config lights it up.
|
|
FEATURE_AUDIO_INGEST: CONFIG_FEATURE_AUDIO_INGEST === true,
|
|
// Bitcoin Connect NWC "Connect wallet" flow (docs/bitcoin-connect-plan.md, #61).
|
|
// Default OFF; only an explicit `true` in the instance config enables it.
|
|
FEATURE_BITCOIN_CONNECT: CONFIG_FEATURE_BITCOIN_CONNECT === true,
|
|
// BitLogin primary sign-in method (js/ui/bitloginModalIntegration.js).
|
|
// Default OFF; only an explicit `true` in the instance config enables it.
|
|
FEATURE_BITLOGIN: CONFIG_FEATURE_BITLOGIN === true,
|
|
// Blossom (nostr-native blob storage) provider (docs/blossom-plan.md, #30).
|
|
// Default OFF; only an explicit `true` in the instance config enables it.
|
|
FEATURE_BLOSSOM_STORAGE: CONFIG_FEATURE_BLOSSOM_STORAGE === true,
|
|
// Blossom videos: publish/lookup the WebTorrent piece-map as a companion Nostr
|
|
// event so P2P works without a hosted .torrent (docs/blossom-torrent-metadata-plan.md).
|
|
// Default OFF; only an explicit `true` in the instance config enables it.
|
|
FEATURE_BLOSSOM_TORRENT_METADATA:
|
|
CONFIG_FEATURE_BLOSSOM_TORRENT_METADATA === true,
|
|
FEATURE_HASHTAG_PREFERENCES: false,
|
|
FEATURE_TRUST_SEEDS: true, // Rollback: disable to drop baseline trust seeds without code changes.
|
|
FEATURE_TRUSTED_HIDE_CONTROLS: true,
|
|
FEATURE_IMPROVED_COMMENT_FETCHING: true, // Default true for comment persistence fixes
|
|
FEATURE_TRENDING_FEED: true, // "Trending" tab: recently-added ranked by view count. Disable to hide the tab.
|
|
FEATURE_MOST_ZAPPED_FEED: true, // "Most Zapped" tab: recently-added ranked by zap total. Disable to hide the tab.
|
|
FEATURE_ZAP_TALLY: true, // bitvid-native preimage-verified zap tally (publish + count). Rollback: set false.
|
|
FEATURE_PLAYLISTS: true, // Creator playlists (#37): channel section + playlist view + add-to-playlist (⋯ menu). Rollback: set false to hide all playlist UI.
|
|
FEATURE_SUBMISSIONS: true, // Structured submissions (#23): admin Submissions tab + forms publish kind-30083 instead of DMs. Rollback: set false to hide the admin tab (forms still publish 30083).
|
|
TRUSTED_MUTE_HIDE_THRESHOLD: DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD,
|
|
TRUSTED_SPAM_HIDE_THRESHOLD: DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD,
|
|
WSS_TRACKERS: DEFAULT_WSS_TRACKERS,
|
|
});
|
|
|
|
const globalScope = typeof globalThis === "object" && globalThis ? globalThis : undefined;
|
|
|
|
const runtimeFlags = (() => {
|
|
if (globalScope && typeof globalScope.__BITVID_RUNTIME_FLAGS__ === "object") {
|
|
return globalScope.__BITVID_RUNTIME_FLAGS__;
|
|
}
|
|
const initial = {
|
|
URL_FIRST_ENABLED: DEFAULT_FLAGS.URL_FIRST_ENABLED,
|
|
FEATURE_WATCH_HISTORY_V2: DEFAULT_FLAGS.FEATURE_WATCH_HISTORY_V2,
|
|
FEATURE_PUBLISH_NIP71: DEFAULT_FLAGS.FEATURE_PUBLISH_NIP71,
|
|
FEATURE_NIP71_MIRROR: DEFAULT_FLAGS.FEATURE_NIP71_MIRROR,
|
|
FEATURE_NIP71_INGEST: DEFAULT_FLAGS.FEATURE_NIP71_INGEST,
|
|
FEATURE_AUDIO_INGEST: DEFAULT_FLAGS.FEATURE_AUDIO_INGEST,
|
|
FEATURE_BITCOIN_CONNECT: DEFAULT_FLAGS.FEATURE_BITCOIN_CONNECT,
|
|
FEATURE_BITLOGIN: DEFAULT_FLAGS.FEATURE_BITLOGIN,
|
|
FEATURE_BLOSSOM_STORAGE: DEFAULT_FLAGS.FEATURE_BLOSSOM_STORAGE,
|
|
FEATURE_BLOSSOM_TORRENT_METADATA:
|
|
DEFAULT_FLAGS.FEATURE_BLOSSOM_TORRENT_METADATA,
|
|
FEATURE_HASHTAG_PREFERENCES: DEFAULT_FLAGS.FEATURE_HASHTAG_PREFERENCES,
|
|
FEATURE_TRUST_SEEDS: DEFAULT_FLAGS.FEATURE_TRUST_SEEDS,
|
|
FEATURE_TRUSTED_HIDE_CONTROLS: DEFAULT_FLAGS.FEATURE_TRUSTED_HIDE_CONTROLS,
|
|
FEATURE_IMPROVED_COMMENT_FETCHING:
|
|
DEFAULT_FLAGS.FEATURE_IMPROVED_COMMENT_FETCHING,
|
|
FEATURE_TRENDING_FEED: DEFAULT_FLAGS.FEATURE_TRENDING_FEED,
|
|
FEATURE_MOST_ZAPPED_FEED: DEFAULT_FLAGS.FEATURE_MOST_ZAPPED_FEED,
|
|
FEATURE_ZAP_TALLY: DEFAULT_FLAGS.FEATURE_ZAP_TALLY,
|
|
FEATURE_PLAYLISTS: DEFAULT_FLAGS.FEATURE_PLAYLISTS,
|
|
FEATURE_SUBMISSIONS: DEFAULT_FLAGS.FEATURE_SUBMISSIONS,
|
|
TRUSTED_MUTE_HIDE_THRESHOLD: DEFAULT_FLAGS.TRUSTED_MUTE_HIDE_THRESHOLD,
|
|
TRUSTED_SPAM_HIDE_THRESHOLD: DEFAULT_FLAGS.TRUSTED_SPAM_HIDE_THRESHOLD,
|
|
WSS_TRACKERS: [...DEFAULT_FLAGS.WSS_TRACKERS],
|
|
};
|
|
if (globalScope) {
|
|
globalScope.__BITVID_RUNTIME_FLAGS__ = initial;
|
|
}
|
|
return initial;
|
|
})();
|
|
|
|
export let URL_FIRST_ENABLED = coerceBoolean(
|
|
runtimeFlags.URL_FIRST_ENABLED,
|
|
DEFAULT_FLAGS.URL_FIRST_ENABLED
|
|
);
|
|
|
|
export let FEATURE_WATCH_HISTORY_V2 = coerceBoolean(
|
|
runtimeFlags.FEATURE_WATCH_HISTORY_V2,
|
|
DEFAULT_FLAGS.FEATURE_WATCH_HISTORY_V2
|
|
);
|
|
|
|
export let FEATURE_PUBLISH_NIP71 = coerceBoolean(
|
|
runtimeFlags.FEATURE_PUBLISH_NIP71,
|
|
DEFAULT_FLAGS.FEATURE_PUBLISH_NIP71
|
|
);
|
|
|
|
export let FEATURE_NIP71_MIRROR = coerceBoolean(
|
|
runtimeFlags.FEATURE_NIP71_MIRROR,
|
|
DEFAULT_FLAGS.FEATURE_NIP71_MIRROR
|
|
);
|
|
|
|
export let FEATURE_NIP71_INGEST = coerceBoolean(
|
|
runtimeFlags.FEATURE_NIP71_INGEST,
|
|
DEFAULT_FLAGS.FEATURE_NIP71_INGEST
|
|
);
|
|
|
|
export let FEATURE_AUDIO_INGEST = coerceBoolean(
|
|
runtimeFlags.FEATURE_AUDIO_INGEST,
|
|
DEFAULT_FLAGS.FEATURE_AUDIO_INGEST
|
|
);
|
|
|
|
export let FEATURE_BITCOIN_CONNECT = coerceBoolean(
|
|
runtimeFlags.FEATURE_BITCOIN_CONNECT,
|
|
DEFAULT_FLAGS.FEATURE_BITCOIN_CONNECT
|
|
);
|
|
|
|
export let FEATURE_BITLOGIN = coerceBoolean(
|
|
runtimeFlags.FEATURE_BITLOGIN,
|
|
DEFAULT_FLAGS.FEATURE_BITLOGIN
|
|
);
|
|
|
|
export let FEATURE_BLOSSOM_STORAGE = coerceBoolean(
|
|
runtimeFlags.FEATURE_BLOSSOM_STORAGE,
|
|
DEFAULT_FLAGS.FEATURE_BLOSSOM_STORAGE
|
|
);
|
|
|
|
export let FEATURE_BLOSSOM_TORRENT_METADATA = coerceBoolean(
|
|
runtimeFlags.FEATURE_BLOSSOM_TORRENT_METADATA,
|
|
DEFAULT_FLAGS.FEATURE_BLOSSOM_TORRENT_METADATA
|
|
);
|
|
|
|
// Max base64 size of an embedded .torrent piece-map before bitvid falls back to
|
|
// URL-only (the companion event is off the feed, so this is a pathological-case
|
|
// guard, not a feature limiter). See docs/blossom-torrent-metadata-plan.md (D4).
|
|
export const BLOSSOM_TORRENT_METADATA_MAX_BASE64 = 65536;
|
|
|
|
export let FEATURE_HASHTAG_PREFERENCES = coerceBoolean(
|
|
runtimeFlags.FEATURE_HASHTAG_PREFERENCES,
|
|
DEFAULT_FLAGS.FEATURE_HASHTAG_PREFERENCES
|
|
);
|
|
|
|
export let FEATURE_TRENDING_FEED = coerceBoolean(
|
|
runtimeFlags.FEATURE_TRENDING_FEED,
|
|
DEFAULT_FLAGS.FEATURE_TRENDING_FEED
|
|
);
|
|
|
|
export let FEATURE_MOST_ZAPPED_FEED = coerceBoolean(
|
|
runtimeFlags.FEATURE_MOST_ZAPPED_FEED,
|
|
DEFAULT_FLAGS.FEATURE_MOST_ZAPPED_FEED
|
|
);
|
|
|
|
export let FEATURE_ZAP_TALLY = coerceBoolean(
|
|
runtimeFlags.FEATURE_ZAP_TALLY,
|
|
DEFAULT_FLAGS.FEATURE_ZAP_TALLY
|
|
);
|
|
|
|
export let FEATURE_PLAYLISTS = coerceBoolean(
|
|
runtimeFlags.FEATURE_PLAYLISTS,
|
|
DEFAULT_FLAGS.FEATURE_PLAYLISTS
|
|
);
|
|
|
|
export let FEATURE_SUBMISSIONS = coerceBoolean(
|
|
runtimeFlags.FEATURE_SUBMISSIONS,
|
|
DEFAULT_FLAGS.FEATURE_SUBMISSIONS
|
|
);
|
|
|
|
export let FEATURE_TRUST_SEEDS = coerceBoolean(
|
|
runtimeFlags.FEATURE_TRUST_SEEDS,
|
|
DEFAULT_FLAGS.FEATURE_TRUST_SEEDS
|
|
);
|
|
|
|
export let FEATURE_TRUSTED_HIDE_CONTROLS = coerceBoolean(
|
|
runtimeFlags.FEATURE_TRUSTED_HIDE_CONTROLS,
|
|
DEFAULT_FLAGS.FEATURE_TRUSTED_HIDE_CONTROLS
|
|
);
|
|
|
|
export let FEATURE_IMPROVED_COMMENT_FETCHING = coerceBoolean(
|
|
runtimeFlags.FEATURE_IMPROVED_COMMENT_FETCHING,
|
|
DEFAULT_FLAGS.FEATURE_IMPROVED_COMMENT_FETCHING
|
|
);
|
|
|
|
export let WSS_TRACKERS = freezeTrackers(
|
|
sanitizeTrackerList(runtimeFlags.WSS_TRACKERS, DEFAULT_FLAGS.WSS_TRACKERS)
|
|
);
|
|
|
|
export let TRUSTED_MUTE_HIDE_THRESHOLD = coerceNonNegativeInteger(
|
|
runtimeFlags.TRUSTED_MUTE_HIDE_THRESHOLD,
|
|
DEFAULT_FLAGS.TRUSTED_MUTE_HIDE_THRESHOLD
|
|
);
|
|
|
|
export let TRUSTED_SPAM_HIDE_THRESHOLD = coerceNonNegativeInteger(
|
|
runtimeFlags.TRUSTED_SPAM_HIDE_THRESHOLD,
|
|
DEFAULT_FLAGS.TRUSTED_SPAM_HIDE_THRESHOLD
|
|
);
|
|
|
|
Object.defineProperty(runtimeFlags, "URL_FIRST_ENABLED", {
|
|
configurable: true,
|
|
enumerable: true,
|
|
get() {
|
|
return URL_FIRST_ENABLED;
|
|
},
|
|
set(next) {
|
|
URL_FIRST_ENABLED = coerceBoolean(next, DEFAULT_FLAGS.URL_FIRST_ENABLED);
|
|
},
|
|
});
|
|
|
|
Object.defineProperty(runtimeFlags, "FEATURE_WATCH_HISTORY_V2", {
|
|
configurable: true,
|
|
enumerable: true,
|
|
get() {
|
|
return FEATURE_WATCH_HISTORY_V2;
|
|
},
|
|
set(next) {
|
|
FEATURE_WATCH_HISTORY_V2 = coerceBoolean(
|
|
next,
|
|
DEFAULT_FLAGS.FEATURE_WATCH_HISTORY_V2
|
|
);
|
|
},
|
|
});
|
|
|
|
Object.defineProperty(runtimeFlags, "FEATURE_PUBLISH_NIP71", {
|
|
configurable: true,
|
|
enumerable: true,
|
|
get() {
|
|
return FEATURE_PUBLISH_NIP71;
|
|
},
|
|
set(next) {
|
|
FEATURE_PUBLISH_NIP71 = coerceBoolean(
|
|
next,
|
|
DEFAULT_FLAGS.FEATURE_PUBLISH_NIP71
|
|
);
|
|
},
|
|
});
|
|
|
|
Object.defineProperty(runtimeFlags, "FEATURE_HASHTAG_PREFERENCES", {
|
|
configurable: true,
|
|
enumerable: true,
|
|
get() {
|
|
return FEATURE_HASHTAG_PREFERENCES;
|
|
},
|
|
set(next) {
|
|
FEATURE_HASHTAG_PREFERENCES = coerceBoolean(
|
|
next,
|
|
DEFAULT_FLAGS.FEATURE_HASHTAG_PREFERENCES
|
|
);
|
|
},
|
|
});
|
|
|
|
Object.defineProperty(runtimeFlags, "FEATURE_TRUST_SEEDS", {
|
|
configurable: true,
|
|
enumerable: true,
|
|
get() {
|
|
return FEATURE_TRUST_SEEDS;
|
|
},
|
|
set(next) {
|
|
FEATURE_TRUST_SEEDS = coerceBoolean(
|
|
next,
|
|
DEFAULT_FLAGS.FEATURE_TRUST_SEEDS
|
|
);
|
|
},
|
|
});
|
|
|
|
Object.defineProperty(runtimeFlags, "FEATURE_TRUSTED_HIDE_CONTROLS", {
|
|
configurable: true,
|
|
enumerable: true,
|
|
get() {
|
|
return FEATURE_TRUSTED_HIDE_CONTROLS;
|
|
},
|
|
set(next) {
|
|
FEATURE_TRUSTED_HIDE_CONTROLS = coerceBoolean(
|
|
next,
|
|
DEFAULT_FLAGS.FEATURE_TRUSTED_HIDE_CONTROLS
|
|
);
|
|
},
|
|
});
|
|
|
|
Object.defineProperty(runtimeFlags, "FEATURE_IMPROVED_COMMENT_FETCHING", {
|
|
configurable: true,
|
|
enumerable: true,
|
|
get() {
|
|
return FEATURE_IMPROVED_COMMENT_FETCHING;
|
|
},
|
|
set(next) {
|
|
FEATURE_IMPROVED_COMMENT_FETCHING = coerceBoolean(
|
|
next,
|
|
DEFAULT_FLAGS.FEATURE_IMPROVED_COMMENT_FETCHING
|
|
);
|
|
},
|
|
});
|
|
|
|
Object.defineProperty(runtimeFlags, "WSS_TRACKERS", {
|
|
configurable: true,
|
|
enumerable: true,
|
|
get() {
|
|
return [...WSS_TRACKERS];
|
|
},
|
|
set(next) {
|
|
WSS_TRACKERS = freezeTrackers(
|
|
sanitizeTrackerList(next, DEFAULT_FLAGS.WSS_TRACKERS)
|
|
);
|
|
},
|
|
});
|
|
|
|
Object.defineProperty(runtimeFlags, "TRUSTED_MUTE_HIDE_THRESHOLD", {
|
|
configurable: true,
|
|
enumerable: true,
|
|
get() {
|
|
return TRUSTED_MUTE_HIDE_THRESHOLD;
|
|
},
|
|
set(next) {
|
|
TRUSTED_MUTE_HIDE_THRESHOLD = coerceNonNegativeInteger(
|
|
next,
|
|
DEFAULT_FLAGS.TRUSTED_MUTE_HIDE_THRESHOLD
|
|
);
|
|
},
|
|
});
|
|
|
|
Object.defineProperty(runtimeFlags, "TRUSTED_SPAM_HIDE_THRESHOLD", {
|
|
configurable: true,
|
|
enumerable: true,
|
|
get() {
|
|
return TRUSTED_SPAM_HIDE_THRESHOLD;
|
|
},
|
|
set(next) {
|
|
TRUSTED_SPAM_HIDE_THRESHOLD = coerceNonNegativeInteger(
|
|
next,
|
|
DEFAULT_FLAGS.TRUSTED_SPAM_HIDE_THRESHOLD
|
|
);
|
|
},
|
|
});
|
|
|
|
// Ensure the runtime object reflects the sanitized defaults immediately.
|
|
runtimeFlags.URL_FIRST_ENABLED = URL_FIRST_ENABLED;
|
|
runtimeFlags.FEATURE_WATCH_HISTORY_V2 = FEATURE_WATCH_HISTORY_V2;
|
|
runtimeFlags.FEATURE_PUBLISH_NIP71 = FEATURE_PUBLISH_NIP71;
|
|
runtimeFlags.FEATURE_HASHTAG_PREFERENCES = FEATURE_HASHTAG_PREFERENCES;
|
|
runtimeFlags.FEATURE_TRUST_SEEDS = FEATURE_TRUST_SEEDS;
|
|
runtimeFlags.FEATURE_TRUSTED_HIDE_CONTROLS = FEATURE_TRUSTED_HIDE_CONTROLS;
|
|
runtimeFlags.FEATURE_IMPROVED_COMMENT_FETCHING =
|
|
FEATURE_IMPROVED_COMMENT_FETCHING;
|
|
runtimeFlags.WSS_TRACKERS = WSS_TRACKERS;
|
|
runtimeFlags.TRUSTED_MUTE_HIDE_THRESHOLD = TRUSTED_MUTE_HIDE_THRESHOLD;
|
|
runtimeFlags.TRUSTED_SPAM_HIDE_THRESHOLD = TRUSTED_SPAM_HIDE_THRESHOLD;
|
|
|
|
export function setUrlFirstEnabled(next) {
|
|
runtimeFlags.URL_FIRST_ENABLED = next;
|
|
return URL_FIRST_ENABLED;
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Card liveness visibility policy (see config/instance-config.js). Resolved on
|
|
// every read so it can be flipped live for A/B feel-testing via the console:
|
|
// window.__BITVID_CARD_LIVENESS_POLICY__ = "hide-foreign" // then refresh/scroll
|
|
// -----------------------------------------------------------------------------
|
|
export const CARD_LIVENESS_POLICIES = Object.freeze([
|
|
"show-pending",
|
|
"hide-foreign",
|
|
"hide-all",
|
|
]);
|
|
|
|
function normalizeLivenessPolicy(value) {
|
|
return typeof value === "string" && CARD_LIVENESS_POLICIES.includes(value)
|
|
? value
|
|
: null;
|
|
}
|
|
|
|
let cardLivenessPolicy =
|
|
normalizeLivenessPolicy(CONFIG_CARD_LIVENESS_POLICY) || "show-pending";
|
|
|
|
export function getCardLivenessPolicy() {
|
|
const override =
|
|
typeof globalThis !== "undefined"
|
|
? normalizeLivenessPolicy(globalThis.__BITVID_CARD_LIVENESS_POLICY__)
|
|
: null;
|
|
return override || cardLivenessPolicy;
|
|
}
|
|
|
|
export function setCardLivenessPolicy(next) {
|
|
const normalized = normalizeLivenessPolicy(next);
|
|
if (normalized) {
|
|
cardLivenessPolicy = normalized;
|
|
if (typeof globalThis !== "undefined") {
|
|
globalThis.__BITVID_CARD_LIVENESS_POLICY__ = normalized;
|
|
}
|
|
}
|
|
return cardLivenessPolicy;
|
|
}
|
|
|
|
// IntersectionObserver rootMargin for the liveness probes — lets cards just below
|
|
// the fold probe ahead of the scroll. Live-overridable for tuning.
|
|
const DEFAULT_LIVENESS_PREFETCH_MARGIN =
|
|
typeof CONFIG_LIVENESS_PROBE_PREFETCH_MARGIN === "string" &&
|
|
CONFIG_LIVENESS_PROBE_PREFETCH_MARGIN.trim()
|
|
? CONFIG_LIVENESS_PROBE_PREFETCH_MARGIN.trim()
|
|
: "0px";
|
|
|
|
export function getLivenessProbePrefetchMargin() {
|
|
const override =
|
|
typeof globalThis !== "undefined"
|
|
? globalThis.__BITVID_LIVENESS_PREFETCH_MARGIN__
|
|
: null;
|
|
if (typeof override === "string" && override.trim()) {
|
|
return override.trim();
|
|
}
|
|
return DEFAULT_LIVENESS_PREFETCH_MARGIN;
|
|
}
|
|
|
|
export function setWssTrackers(next) {
|
|
runtimeFlags.WSS_TRACKERS = next;
|
|
return WSS_TRACKERS;
|
|
}
|
|
|
|
export function setTrustSeedsEnabled(next) {
|
|
runtimeFlags.FEATURE_TRUST_SEEDS = next;
|
|
return FEATURE_TRUST_SEEDS;
|
|
}
|
|
|
|
export function setTrustedHideControlsEnabled(next) {
|
|
runtimeFlags.FEATURE_TRUSTED_HIDE_CONTROLS = next;
|
|
return FEATURE_TRUSTED_HIDE_CONTROLS;
|
|
}
|
|
|
|
export function setImprovedCommentFetchingEnabled(next) {
|
|
runtimeFlags.FEATURE_IMPROVED_COMMENT_FETCHING = next;
|
|
return FEATURE_IMPROVED_COMMENT_FETCHING;
|
|
}
|
|
|
|
export function setHashtagPreferencesEnabled(next) {
|
|
runtimeFlags.FEATURE_HASHTAG_PREFERENCES = next;
|
|
return FEATURE_HASHTAG_PREFERENCES;
|
|
}
|
|
|
|
export function resetRuntimeFlags() {
|
|
setUrlFirstEnabled(DEFAULT_FLAGS.URL_FIRST_ENABLED);
|
|
setWatchHistoryV2Enabled(DEFAULT_FLAGS.FEATURE_WATCH_HISTORY_V2);
|
|
setHashtagPreferencesEnabled(DEFAULT_FLAGS.FEATURE_HASHTAG_PREFERENCES);
|
|
setTrustSeedsEnabled(DEFAULT_FLAGS.FEATURE_TRUST_SEEDS);
|
|
setTrustedHideControlsEnabled(DEFAULT_FLAGS.FEATURE_TRUSTED_HIDE_CONTROLS);
|
|
setImprovedCommentFetchingEnabled(
|
|
DEFAULT_FLAGS.FEATURE_IMPROVED_COMMENT_FETCHING
|
|
);
|
|
setTrustedMuteHideThreshold(DEFAULT_FLAGS.TRUSTED_MUTE_HIDE_THRESHOLD);
|
|
setTrustedSpamHideThreshold(DEFAULT_FLAGS.TRUSTED_SPAM_HIDE_THRESHOLD);
|
|
setWssTrackers(DEFAULT_FLAGS.WSS_TRACKERS);
|
|
}
|
|
|
|
export const RUNTIME_FLAGS = runtimeFlags;
|
|
|
|
export function setWatchHistoryV2Enabled(next) {
|
|
runtimeFlags.FEATURE_WATCH_HISTORY_V2 = next;
|
|
return FEATURE_WATCH_HISTORY_V2;
|
|
}
|
|
|
|
export function setTrustedMuteHideThreshold(next) {
|
|
runtimeFlags.TRUSTED_MUTE_HIDE_THRESHOLD = next;
|
|
return TRUSTED_MUTE_HIDE_THRESHOLD;
|
|
}
|
|
|
|
export function setTrustedSpamHideThreshold(next) {
|
|
runtimeFlags.TRUSTED_SPAM_HIDE_THRESHOLD = next;
|
|
return TRUSTED_SPAM_HIDE_THRESHOLD;
|
|
}
|
|
|
|
export function getTrustedMuteHideThreshold() {
|
|
return TRUSTED_MUTE_HIDE_THRESHOLD;
|
|
}
|
|
|
|
export function getTrustedSpamHideThreshold() {
|
|
return TRUSTED_SPAM_HIDE_THRESHOLD;
|
|
}
|
|
|
|
export const FEED_TYPES = Object.freeze({
|
|
RECENT: "most-recent-videos",
|
|
FOR_YOU: "for-you",
|
|
KIDS: "kids",
|
|
EXPLORE: "explore",
|
|
TRENDING: "trending",
|
|
MOST_ZAPPED: "most-zapped",
|
|
SUBSCRIPTIONS: "subscriptions",
|
|
HISTORY: "history",
|
|
CHANNEL: "channel-profile",
|
|
DOCS: "docs",
|
|
SEARCH: "search",
|
|
PLAYLIST: "playlist",
|
|
});
|