Add URL-aware playback delegation with fallback handling

This commit is contained in:
thePR0M3TH3AN committed 2025-09-22 12:58:51 -04:00
1 parent ad5f8958c5
commit 2ed1cc50db
3 files changed
+244 -147

No files matched your search

+217 -141
View File
@@ -643,15 +643,39 @@ class bitvidApp {
// 9) Event delegation on the video list container for playing videos
if (this.videoList) {
this.videoList.addEventListener("click", (event) => {
const trigger = event.target.closest("[data-video-id]");
if (trigger) {
if (event.button === 0 && !event.ctrlKey && !event.metaKey) {
event.preventDefault();
const eventId = trigger.getAttribute("data-video-id");
if (eventId) {
this.playVideoByEventId(eventId);
this.videoList.addEventListener("click", async (event) => {
const trigger = event.target.closest(
"[data-play-url], [data-play-magnet]"
);
if (!trigger) {
return;
}
if (event.button === 0 && !event.ctrlKey && !event.metaKey) {
event.preventDefault();
const decodeDataValue = (value) => {
if (!value) return "";
try {
return decodeURIComponent(value);
} catch (err) {
console.warn("Failed to decode data attribute:", err);
return "";
}
};
const encodedUrl = trigger.getAttribute("data-play-url") || "";
const encodedMagnet =
trigger.getAttribute("data-play-magnet") || "";
const url = decodeDataValue(encodedUrl);
const magnet = decodeDataValue(encodedMagnet);
const eventId = trigger.getAttribute("data-video-id");
if (eventId) {
await this.playVideoWithFallback({ eventId, url, magnet });
} else {
await this.playVideoWithFallback({ url, magnet });
}
}
});
@@ -1166,6 +1190,11 @@ class bitvidApp {
const authorSet = new Set();
// 3) Build each card
const encodeDataValue = (value) =>
typeof value === "string" && value.length > 0
? encodeURIComponent(value)
: "";
videos.forEach((video, index) => {
if (!video.id || !video.title) {
console.error("Video missing ID/title:", video);
@@ -1240,8 +1269,8 @@ class bitvidApp {
`
: "";
const encodedMagnet = encodeURIComponent(video.magnet || "");
const encodedUrl = encodeURIComponent(video.url || "");
const encodedMagnet = encodeDataValue(video.magnet);
const encodedUrl = encodeDataValue(video.url);
const cardHtml = `
<div class="video-card bg-gray-900 rounded-lg overflow-hidden shadow-lg hover:shadow-2xl transition-all duration-300 ${highlightClass}">
<!-- The clickable link to play video -->
@@ -1663,135 +1692,176 @@ class bitvidApp {
}
/**
* Helper to open a video by event ID (like ?v=...).
* Unified playback helper that prefers HTTP URL sources
* and falls back to WebTorrent when needed.
*/
async playVideoByEventId(eventId) {
// 1) Event-level blacklist check
if (this.blacklistedEventIds.has(eventId)) {
this.showError("This content has been removed or is not allowed.");
return;
}
async playVideoWithFallback({
url: rawUrl = "",
magnet: rawMagnet = "",
eventId = null,
} = {}) {
const sanitize = (value) =>
typeof value === "string" ? value.trim() : "";
let trimmedUrl = sanitize(rawUrl);
let trimmedMagnet = sanitize(rawMagnet);
try {
// Attempt to get the video from local cache or fetch
let video = this.videosMap.get(eventId);
if (!video) {
video = await this.getOldEventById(eventId);
}
if (!video) {
this.showError("Video not found.");
return;
}
let video = null;
// 2) Author-level blacklist check
const authorNpub = this.safeEncodeNpub(video.pubkey) || video.pubkey;
if (initialBlacklist.includes(authorNpub)) {
this.showError("This content has been removed or is not allowed.");
return;
}
// 3) Whitelist check if enabled
if (isWhitelistEnabled && !initialWhitelist.includes(authorNpub)) {
this.showError("This content is not from a whitelisted author.");
return;
}
// Handle private videos (decrypt if owner is the current user)
if (
video.isPrivate &&
video.pubkey === this.pubkey &&
!video.alreadyDecrypted
) {
video.magnet = fakeDecrypt(video.magnet);
video.alreadyDecrypted = true;
}
const trimmedMagnet =
typeof video.magnet === "string" ? video.magnet.trim() : "";
const trimmedUrl =
typeof video.url === "string" ? video.url.trim() : "";
const hasMagnet = !!trimmedMagnet;
const hasUrl = !!trimmedUrl;
if (!hasMagnet && !hasUrl) {
this.showError("This video has no playable source.");
return;
}
video.magnet = trimmedMagnet;
video.url = trimmedUrl;
this.currentVideo = video;
this.currentMagnetUri = hasMagnet ? trimmedMagnet : null;
this.showModalWithPoster();
if (this.copyMagnetBtn) {
this.copyMagnetBtn.disabled = !hasMagnet;
this.copyMagnetBtn.setAttribute(
"aria-disabled",
(!hasMagnet).toString()
);
this.copyMagnetBtn.classList.toggle("opacity-50", !hasMagnet);
this.copyMagnetBtn.classList.toggle("cursor-not-allowed", !hasMagnet);
}
// Update ?v= param in the URL
const nevent = window.NostrTools.nip19.neventEncode({ id: eventId });
const newUrl = `${window.location.pathname}?v=${encodeURIComponent(
nevent
)}`;
window.history.pushState({}, "", newUrl);
// Fetch author profile
let creatorProfile = {
name: "Unknown",
picture: `https://robohash.org/${video.pubkey}`,
};
try {
const userEvents = await nostrClient.pool.list(nostrClient.relays, [
{ kinds: [0], authors: [video.pubkey], limit: 1 },
]);
if (userEvents.length > 0 && userEvents[0]?.content) {
const data = JSON.parse(userEvents[0].content);
creatorProfile = {
name: data.name || data.display_name || "Unknown",
picture: data.picture || `https://robohash.org/${video.pubkey}`,
};
if (eventId) {
if (this.blacklistedEventIds.has(eventId)) {
this.showError("This content has been removed or is not allowed.");
return;
}
video = this.videosMap.get(eventId);
if (!video) {
video = await this.getOldEventById(eventId);
}
if (!video) {
this.showError("Video not found.");
return;
}
const authorNpub = this.safeEncodeNpub(video.pubkey) || video.pubkey;
if (initialBlacklist.includes(authorNpub)) {
this.showError("This content has been removed or is not allowed.");
return;
}
if (isWhitelistEnabled && !initialWhitelist.includes(authorNpub)) {
this.showError("This content is not from a whitelisted author.");
return;
}
if (
video.isPrivate &&
video.pubkey === this.pubkey &&
!video.alreadyDecrypted
) {
video.magnet = fakeDecrypt(video.magnet);
video.alreadyDecrypted = true;
}
if (!trimmedMagnet && typeof video.magnet === "string") {
trimmedMagnet = video.magnet.trim();
}
if (!trimmedUrl && typeof video.url === "string") {
trimmedUrl = video.url.trim();
}
const hasMagnet = !!trimmedMagnet;
const hasUrl = !!trimmedUrl;
if (!hasMagnet && !hasUrl) {
this.showError("This video has no playable source.");
return;
}
video.magnet = trimmedMagnet;
video.url = trimmedUrl;
this.currentVideo = video;
this.showModalWithPoster();
if (this.copyMagnetBtn) {
this.copyMagnetBtn.disabled = !hasMagnet;
this.copyMagnetBtn.setAttribute(
"aria-disabled",
(!hasMagnet).toString()
);
this.copyMagnetBtn.classList.toggle("opacity-50", !hasMagnet);
this.copyMagnetBtn.classList.toggle(
"cursor-not-allowed",
!hasMagnet
);
}
const nevent = window.NostrTools.nip19.neventEncode({ id: eventId });
const newUrl = `${window.location.pathname}?v=${encodeURIComponent(
nevent
)}`;
window.history.pushState({}, "", newUrl);
let creatorProfile = {
name: "Unknown",
picture: `https://robohash.org/${video.pubkey}`,
};
try {
const userEvents = await nostrClient.pool.list(nostrClient.relays, [
{ kinds: [0], authors: [video.pubkey], limit: 1 },
]);
if (userEvents.length > 0 && userEvents[0]?.content) {
const data = JSON.parse(userEvents[0].content);
creatorProfile = {
name: data.name || data.display_name || "Unknown",
picture: data.picture || `https://robohash.org/${video.pubkey}`,
};
}
} catch (error) {
this.log("Error fetching creator profile:", error);
}
const creatorNpub = this.safeEncodeNpub(video.pubkey) || video.pubkey;
if (this.videoTitle) {
this.videoTitle.textContent = video.title || "Untitled";
}
if (this.videoDescription) {
this.videoDescription.textContent =
video.description || "No description available.";
}
if (this.videoTimestamp) {
this.videoTimestamp.textContent = this.formatTimeAgo(
video.created_at
);
}
if (this.creatorName) {
this.creatorName.textContent = creatorProfile.name;
}
if (this.creatorNpub) {
this.creatorNpub.textContent = `${creatorNpub.slice(
0,
8
)}...${creatorNpub.slice(-4)}`;
}
if (this.creatorAvatar) {
this.creatorAvatar.src = creatorProfile.picture;
this.creatorAvatar.alt = creatorProfile.name;
}
} else {
const hasMagnet = !!trimmedMagnet;
const hasUrl = !!trimmedUrl;
if (!hasMagnet && !hasUrl) {
this.showError("This video has no playable source.");
return;
}
this.showModalWithPoster();
if (this.copyMagnetBtn) {
this.copyMagnetBtn.disabled = !hasMagnet;
this.copyMagnetBtn.setAttribute(
"aria-disabled",
(!hasMagnet).toString()
);
this.copyMagnetBtn.classList.toggle("opacity-50", !hasMagnet);
this.copyMagnetBtn.classList.toggle(
"cursor-not-allowed",
!hasMagnet
);
}
} catch (error) {
this.log("Error fetching creator profile:", error);
}
// Update UI fields
const creatorNpub = this.safeEncodeNpub(video.pubkey) || video.pubkey;
if (this.videoTitle) {
this.videoTitle.textContent = video.title || "Untitled";
}
if (this.videoDescription) {
this.videoDescription.textContent =
video.description || "No description available.";
}
if (this.videoTimestamp) {
this.videoTimestamp.textContent = this.formatTimeAgo(video.created_at);
}
if (this.creatorName) {
this.creatorName.textContent = creatorProfile.name;
}
if (this.creatorNpub) {
this.creatorNpub.textContent = `${creatorNpub.slice(
0,
8
)}...${creatorNpub.slice(-4)}`;
}
if (this.creatorAvatar) {
this.creatorAvatar.src = creatorProfile.picture;
this.creatorAvatar.alt = creatorProfile.name;
}
const hasMagnetFinal = !!trimmedMagnet;
const hasUrlFinal = !!trimmedUrl;
this.currentMagnetUri = hasMagnetFinal ? trimmedMagnet : null;
// Cleanup any previous WebTorrent streams, then start fresh playback
await torrentClient.cleanup();
if (!hasMagnet) {
if (!hasMagnetFinal) {
if (this.modalPeers) {
this.modalPeers.textContent = "";
}
@@ -1808,16 +1878,18 @@ class bitvidApp {
const storedUnmuted = localStorage.getItem("unmutedAutoplay");
const userWantsUnmuted = storedUnmuted === "true";
this.modalVideo.muted = !userWantsUnmuted;
this.modalVideo.addEventListener("volumechange", () => {
localStorage.setItem(
"unmutedAutoplay",
(!this.modalVideo.muted).toString()
);
});
if (this.modalVideo) {
this.modalVideo.muted = !userWantsUnmuted;
this.modalVideo.addEventListener("volumechange", () => {
localStorage.setItem(
"unmutedAutoplay",
(!this.modalVideo.muted).toString()
);
});
}
const attemptAutoplay = () => {
if (!this.modalVideo) return;
this.modalVideo.play().catch((err) => {
this.log("Autoplay failed:", err);
if (!this.modalVideo.muted) {
@@ -1873,7 +1945,7 @@ class bitvidApp {
};
const startTorrentPlayback = async () => {
if (!hasMagnet) {
if (!hasMagnetFinal || !this.modalVideo) {
return null;
}
const cacheBustedMagnet = `${trimmedMagnet}&ts=${Date.now()}`;
@@ -1889,7 +1961,7 @@ class bitvidApp {
return torrentInstance;
};
if (hasUrl) {
if (hasUrlFinal && this.modalVideo) {
if (this.modalStatus) {
this.modalStatus.textContent = "Loading video...";
}
@@ -1904,7 +1976,7 @@ class bitvidApp {
this.modalVideo.src = trimmedUrl;
this.modalVideo.load();
if (hasMagnet) {
if (hasMagnetFinal) {
const fallbackToTorrent = async () => {
this.modalVideo.removeEventListener("error", fallbackToTorrent);
this.log("HTTP playback failed, falling back to WebTorrent.");
@@ -1927,11 +1999,15 @@ class bitvidApp {
attemptAutoplay();
} catch (error) {
this.log("Error in playVideoByEventId:", error);
this.log("Error in playVideoWithFallback:", error);
this.showError(`Playback error: ${error.message}`);
}
}
async playVideoByEventId(eventId) {
return this.playVideoWithFallback({ eventId });
}
/**
* Simple helper to safely encode an npub.
*/
+20 -4
View File
@@ -240,6 +240,11 @@ async function loadUserVideos(pubkey) {
const fragment = document.createDocumentFragment();
const allKnownEventsArray = Array.from(nostrClient.allEvents.values());
const encodeDataValue = (value) =>
typeof value === "string" && value.length > 0
? encodeURIComponent(value)
: "";
videos.forEach((video, index) => {
// Decrypt if user owns a private video
if (
@@ -331,11 +336,18 @@ async function loadUserVideos(pubkey) {
"duration-300"
);
const encodedUrl = encodeURIComponent(video.url || "");
const encodedMagnet = encodeURIComponent(video.magnet || "");
const encodedUrl = encodeDataValue(video.url);
const encodedMagnet = encodeDataValue(video.magnet);
const rawUrl = typeof video.url === "string" ? video.url : "";
const rawMagnet = typeof video.magnet === "string" ? video.magnet : "";
cardEl.innerHTML = `
<div class="cursor-pointer relative group">
<div
class="cursor-pointer relative group"
data-video-id="${video.id}"
data-play-url="${encodedUrl}"
data-play-magnet="${encodedMagnet}"
>
<div class="ratio-16-9">
<img
src="${fallbackThumb}"
@@ -364,7 +376,11 @@ async function loadUserVideos(pubkey) {
// Clicking the card => open the video modal
cardEl.addEventListener("click", () => {
app.playVideoByEventId(video.id);
app.playVideoWithFallback({
eventId: video.id,
url: rawUrl,
magnet: rawMagnet,
});
});
fragment.appendChild(cardEl);
+7 -2
View File
@@ -250,6 +250,11 @@ class SubscriptionsManager {
// Only declare localAuthorSet once
const localAuthorSet = new Set();
const encodeDataValue = (value) =>
typeof value === "string" && value.length > 0
? encodeURIComponent(value)
: "";
videos.forEach((video, index) => {
if (!video.id || !video.title) {
console.error("Missing ID or title:", video);
@@ -329,8 +334,8 @@ class SubscriptionsManager {
const safeTitle = window.app?.escapeHTML(video.title) || "Untitled";
const safeThumb = window.app?.escapeHTML(video.thumbnail) || "";
const encodedMagnet = encodeURIComponent(video.magnet || "");
const encodedUrl = encodeURIComponent(video.url || "");
const encodedMagnet = encodeDataValue(video.magnet);
const encodedUrl = encodeDataValue(video.url);
const cardHtml = `
<div class="video-card bg-gray-900 rounded-lg overflow-hidden shadow-lg hover:shadow-2xl transition-all duration-300 ${highlightClass}">
<a