mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-04 20:02:26 +00:00
update
This commit is contained in:
1 parent
589925f1fe
commit
ef29bb6eb3
97 files changed
+46855
No files matched your search
Vendored
+393
@@ -0,0 +1,393 @@
|
||||
import { devLogger, userLogger } from "../utils/logger.js";
|
||||
import { SHORT_TIMEOUT_MS } from "../constants.js";
|
||||
|
||||
export default class VideoModalController {
|
||||
constructor({
|
||||
getVideoModal,
|
||||
callbacks: {
|
||||
showError,
|
||||
showSuccess,
|
||||
showStatus,
|
||||
getLastModalTrigger,
|
||||
setLastModalTrigger,
|
||||
getCurrentVideo,
|
||||
getPlaySource,
|
||||
getStreamHealthSnapshots,
|
||||
getCachedUrlHealth,
|
||||
handleCopyMagnet,
|
||||
openShareNostrModal,
|
||||
playVideoWithFallback,
|
||||
attachMoreMenuHandlers,
|
||||
} = {},
|
||||
}) {
|
||||
this.getVideoModal = getVideoModal;
|
||||
this.showError = showError;
|
||||
this.showSuccess = showSuccess;
|
||||
this.showStatus = showStatus;
|
||||
this.getLastModalTrigger = getLastModalTrigger;
|
||||
this.setLastModalTrigger = setLastModalTrigger;
|
||||
this.getCurrentVideo = getCurrentVideo;
|
||||
this.getPlaySource = getPlaySource;
|
||||
this.getStreamHealthSnapshots = getStreamHealthSnapshots;
|
||||
this.getCachedUrlHealth = getCachedUrlHealth;
|
||||
this.handleCopyMagnetCallback = handleCopyMagnet;
|
||||
this.openShareNostrModalCallback = openShareNostrModal;
|
||||
this.playVideoWithFallbackCallback = playVideoWithFallback;
|
||||
this.attachMoreMenuHandlersCallback = attachMoreMenuHandlers;
|
||||
|
||||
this.videoModalReadyPromise = null;
|
||||
}
|
||||
|
||||
get videoModal() {
|
||||
return this.getVideoModal ? this.getVideoModal() : null;
|
||||
}
|
||||
|
||||
log(message, ...args) {
|
||||
devLogger.log("[VideoModalController]", message, ...args);
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
if (!this.videoModal) {
|
||||
return;
|
||||
}
|
||||
|
||||
const modalRoot =
|
||||
typeof this.videoModal.getRoot === "function"
|
||||
? this.videoModal.getRoot()
|
||||
: null;
|
||||
|
||||
if (modalRoot && this.attachMoreMenuHandlersCallback) {
|
||||
this.attachMoreMenuHandlersCallback(modalRoot);
|
||||
}
|
||||
|
||||
if (typeof this.videoModal.addEventListener === "function") {
|
||||
this.videoModal.addEventListener("video:share-nostr", (event) => {
|
||||
this.handleShareNostr(event);
|
||||
});
|
||||
|
||||
this.videoModal.addEventListener("video:copy-cdn", (event) => {
|
||||
this.handleCopyCdn(event);
|
||||
});
|
||||
|
||||
this.videoModal.addEventListener("video:copy-magnet", () => {
|
||||
if (this.handleCopyMagnetCallback) {
|
||||
this.handleCopyMagnetCallback();
|
||||
}
|
||||
});
|
||||
|
||||
this.videoModal.addEventListener("playback:switch-source", (event) => {
|
||||
this.handleSourceSwitch(event);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleShareNostr(event) {
|
||||
if (this.openShareNostrModalCallback) {
|
||||
this.openShareNostrModalCallback({
|
||||
video: event?.detail?.video || null,
|
||||
triggerElement: event?.detail?.trigger || null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleCopyCdn(event) {
|
||||
const video =
|
||||
event?.detail?.video ||
|
||||
(this.getCurrentVideo ? this.getCurrentVideo() : null);
|
||||
const url = video?.url || "";
|
||||
if (!url) {
|
||||
if (this.showError) {
|
||||
this.showError("No CDN link available to copy.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
navigator.clipboard
|
||||
.writeText(url)
|
||||
.then(() => {
|
||||
if (this.showSuccess) {
|
||||
this.showSuccess("CDN link copied to clipboard!");
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (this.showError) {
|
||||
this.showError("Failed to copy CDN link.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleSourceSwitch(event) {
|
||||
const detail = event?.detail || {};
|
||||
const { source } = detail;
|
||||
if (!source) {
|
||||
return;
|
||||
}
|
||||
|
||||
const modalVideo = detail?.video || null;
|
||||
const fallbackVideo = this.getCurrentVideo ? this.getCurrentVideo() : null;
|
||||
const video = {
|
||||
...(fallbackVideo || {}),
|
||||
...(modalVideo || {}),
|
||||
};
|
||||
const urlCandidate =
|
||||
typeof video.url === "string" ? video.url.trim() : "";
|
||||
const magnetCandidate =
|
||||
typeof video.magnet === "string" ? video.magnet.trim() : "";
|
||||
|
||||
if (!modalVideo && !fallbackVideo) {
|
||||
devLogger.warn(
|
||||
"[VideoModalController] Playback source switch missing video data.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const magnetAvailable = Boolean(magnetCandidate);
|
||||
const streamHealthSnapshots = this.getStreamHealthSnapshots
|
||||
? this.getStreamHealthSnapshots()
|
||||
: null;
|
||||
|
||||
const cachedStreamHealth =
|
||||
video?.id && streamHealthSnapshots instanceof Map
|
||||
? streamHealthSnapshots.get(video.id)
|
||||
: null;
|
||||
const cachedPeers = Number.isFinite(cachedStreamHealth?.peers)
|
||||
? cachedStreamHealth.peers
|
||||
: null;
|
||||
const hasActivePeers = cachedPeers === null ? null : cachedPeers > 0;
|
||||
|
||||
const cachedUrlHealth =
|
||||
video?.id && urlCandidate && this.getCachedUrlHealth
|
||||
? this.getCachedUrlHealth(video.id, urlCandidate)
|
||||
: null;
|
||||
const cdnUnavailable =
|
||||
!urlCandidate ||
|
||||
["offline", "timeout"].includes(cachedUrlHealth?.status);
|
||||
|
||||
if (source === "torrent" && !magnetAvailable) {
|
||||
userLogger.warn(
|
||||
"[VideoModalController] Unable to switch to torrent playback: missing magnet.",
|
||||
);
|
||||
if (this.showError) {
|
||||
this.showError(
|
||||
"Torrent playback is unavailable for this video. No magnet was provided.",
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (source === "torrent" && hasActivePeers === false) {
|
||||
userLogger.warn(
|
||||
"[VideoModalController] Switching to torrent playback despite 0 active peers detected.",
|
||||
);
|
||||
if (this.showStatus) {
|
||||
this.showStatus(
|
||||
"Warning: No peers detected. Playback may fail or stall.",
|
||||
{ autoHideMs: SHORT_TIMEOUT_MS },
|
||||
);
|
||||
}
|
||||
// Proceed anyway
|
||||
}
|
||||
|
||||
if (source === "url" && cdnUnavailable) {
|
||||
userLogger.warn(
|
||||
"[VideoModalController] Unable to switch to CDN playback: URL unavailable.",
|
||||
);
|
||||
if (this.showError) {
|
||||
this.showError(
|
||||
"CDN playback is unavailable right now, staying on the torrent stream.",
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const currentSource = this.getPlaySource ? this.getPlaySource() : null;
|
||||
if (currentSource && source === currentSource) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.playVideoWithFallbackCallback) {
|
||||
this.playVideoWithFallbackCallback({
|
||||
url: urlCandidate,
|
||||
magnet: magnetCandidate,
|
||||
forcedSource: source,
|
||||
}).catch((error) => {
|
||||
devLogger.warn(
|
||||
"[VideoModalController] Failed to switch playback source:",
|
||||
error,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the modal and set the "Please stand by" poster on the video.
|
||||
*/
|
||||
async showModalWithPoster(video = null, options = {}) {
|
||||
if (!this.videoModal) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(options || {}, "trigger")) {
|
||||
if (this.setLastModalTrigger) {
|
||||
this.setLastModalTrigger(options.trigger);
|
||||
}
|
||||
}
|
||||
|
||||
const targetVideo =
|
||||
video || (this.getCurrentVideo ? this.getCurrentVideo() : null);
|
||||
if (!targetVideo) {
|
||||
this.log("Skipping video modal open; no target video is available.");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const { root } = await this.ensureVideoModalReady({
|
||||
ensureVideoElement: true,
|
||||
});
|
||||
|
||||
if (!this.videoModal) {
|
||||
return root || null;
|
||||
}
|
||||
|
||||
const trigger = this.getLastModalTrigger
|
||||
? this.getLastModalTrigger()
|
||||
: null;
|
||||
this.videoModal.open(targetVideo, {
|
||||
triggerElement: trigger,
|
||||
});
|
||||
this.applyModalLoadingPoster();
|
||||
|
||||
return (
|
||||
root ||
|
||||
(typeof this.videoModal.getRoot === "function"
|
||||
? this.videoModal.getRoot()
|
||||
: null)
|
||||
);
|
||||
} catch (error) {
|
||||
devLogger.error(
|
||||
"[VideoModalController] Failed to open the video modal before playback:",
|
||||
error,
|
||||
);
|
||||
if (this.showError) {
|
||||
this.showError("Could not open the video player. Please try again.");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
applyModalLoadingPoster() {
|
||||
if (!this.videoModal) {
|
||||
return;
|
||||
}
|
||||
this.videoModal.applyLoadingPoster();
|
||||
}
|
||||
|
||||
forceRemoveModalPoster(reason = "manual-clear") {
|
||||
if (!this.videoModal) {
|
||||
return false;
|
||||
}
|
||||
return this.videoModal.forceRemovePoster(reason);
|
||||
}
|
||||
|
||||
async ensureVideoModalReady({ ensureVideoElement = false } = {}) {
|
||||
if (!this.videoModal) {
|
||||
throw new Error("Video modal instance is not available.");
|
||||
}
|
||||
|
||||
const getRoot = () =>
|
||||
typeof this.videoModal.getRoot === "function"
|
||||
? this.videoModal.getRoot()
|
||||
: null;
|
||||
const getVideoElement = () =>
|
||||
typeof this.videoModal.getVideoElement === "function"
|
||||
? this.videoModal.getVideoElement()
|
||||
: null;
|
||||
|
||||
const existingRoot = getRoot();
|
||||
const existingVideoElement = getVideoElement();
|
||||
const rootConnected = Boolean(existingRoot && existingRoot.isConnected);
|
||||
const hasVideoElement = Boolean(existingVideoElement);
|
||||
const videoConnected = Boolean(
|
||||
existingVideoElement && existingVideoElement.isConnected,
|
||||
);
|
||||
|
||||
const needsRehydrate =
|
||||
!rootConnected || !hasVideoElement || !videoConnected;
|
||||
|
||||
if (!needsRehydrate) {
|
||||
if (
|
||||
existingVideoElement &&
|
||||
this.videoModal &&
|
||||
typeof this.videoModal.setVideoElement === "function"
|
||||
) {
|
||||
const currentModalVideo =
|
||||
typeof this.videoModal.getVideoElement === "function"
|
||||
? this.videoModal.getVideoElement()
|
||||
: null;
|
||||
if (currentModalVideo !== existingVideoElement) {
|
||||
this.videoModal.setVideoElement(existingVideoElement);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
root: existingRoot,
|
||||
videoElement: existingVideoElement,
|
||||
};
|
||||
}
|
||||
|
||||
if (!videoConnected) {
|
||||
if (
|
||||
this.videoModal &&
|
||||
typeof this.videoModal.setVideoElement === "function"
|
||||
) {
|
||||
this.videoModal.setVideoElement(null);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.videoModalReadyPromise) {
|
||||
if (typeof this.videoModal.load !== "function") {
|
||||
throw new Error("Video modal does not expose a load() method.");
|
||||
}
|
||||
this.videoModalReadyPromise = Promise.resolve(this.videoModal.load());
|
||||
}
|
||||
|
||||
try {
|
||||
await this.videoModalReadyPromise;
|
||||
} catch (error) {
|
||||
this.videoModalReadyPromise = null;
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.videoModalReadyPromise = null;
|
||||
|
||||
const readyRoot = getRoot();
|
||||
const readyVideoElement = getVideoElement();
|
||||
const readyVideoConnected = Boolean(
|
||||
readyVideoElement && readyVideoElement.isConnected,
|
||||
);
|
||||
|
||||
if (readyVideoConnected) {
|
||||
if (
|
||||
this.videoModal &&
|
||||
typeof this.videoModal.setVideoElement === "function"
|
||||
) {
|
||||
this.videoModal.setVideoElement(readyVideoElement);
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
this.videoModal &&
|
||||
typeof this.videoModal.setVideoElement === "function"
|
||||
) {
|
||||
this.videoModal.setVideoElement(null);
|
||||
}
|
||||
}
|
||||
|
||||
if (ensureVideoElement && !readyVideoConnected) {
|
||||
throw new Error("Video modal video element is missing after load().");
|
||||
}
|
||||
|
||||
return {
|
||||
root: readyRoot,
|
||||
videoElement: readyVideoConnected ? readyVideoElement : null,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user