Support URL-only videos and update playback fallback

This commit is contained in:
thePR0M3TH3AN committed 2025-09-22 12:05:15 -04:00
1 parent 7350f43ca7
commit 9014371263
4 files changed
+219 -78

No files matched your search

+168 -64
View File
@@ -644,13 +644,14 @@ class bitvidApp {
// 9) Event delegation on the video list container for playing videos
if (this.videoList) {
this.videoList.addEventListener("click", (event) => {
const magnetTrigger = event.target.closest("[data-play-magnet]");
if (magnetTrigger) {
// For a normal left-click (button 0, no Ctrl/Cmd), prevent navigation:
const trigger = event.target.closest("[data-video-id]");
if (trigger) {
if (event.button === 0 && !event.ctrlKey && !event.metaKey) {
event.preventDefault(); // Stop browser from following the href
const magnet = magnetTrigger.dataset.playMagnet;
this.playVideo(magnet);
event.preventDefault();
const eventId = trigger.getAttribute("data-video-id");
if (eventId) {
this.playVideoByEventId(eventId);
}
}
}
});
@@ -1239,12 +1240,16 @@ class bitvidApp {
`
: "";
const encodedMagnet = encodeURIComponent(video.magnet || "");
const encodedUrl = encodeURIComponent(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 -->
<a
href="${shareUrl}"
data-play-magnet="${encodeURIComponent(video.magnet)}"
data-video-id="${video.id}"
data-play-url="${encodedUrl}"
data-play-magnet="${encodedMagnet}"
class="block cursor-pointer relative group"
>
<div class="ratio-16-9">
@@ -1259,7 +1264,9 @@ class bitvidApp {
<!-- Title triggers the video modal as well -->
<h3
class="text-lg font-bold text-white line-clamp-2 hover:text-blue-400 cursor-pointer mb-3"
data-play-magnet="${encodeURIComponent(video.magnet)}"
data-video-id="${video.id}"
data-play-url="${encodedUrl}"
data-play-magnet="${encodedMagnet}"
>
${this.escapeHTML(video.title)}
</h3>
@@ -1469,6 +1476,10 @@ class bitvidApp {
"New Magnet? (blank=keep existing)",
video.magnet
);
const newUrl = prompt(
"New URL? (blank=keep existing)",
video.url || ""
);
const newThumb = prompt(
"New Thumbnail? (blank=keep existing)",
video.thumbnail
@@ -1484,6 +1495,7 @@ class bitvidApp {
!newTitle || !newTitle.trim() ? video.title : newTitle.trim();
const magnet =
!newMagnet || !newMagnet.trim() ? video.magnet : newMagnet.trim();
const url = !newUrl || !newUrl.trim() ? video.url : newUrl.trim();
const thumbnail =
!newThumb || !newThumb.trim() ? video.thumbnail : newThumb.trim();
const description =
@@ -1495,6 +1507,7 @@ class bitvidApp {
// isPrivate: wantPrivate,
title,
magnet,
url,
thumbnail,
description,
mode: isDevMode ? "dev" : "live",
@@ -1693,10 +1706,35 @@ class bitvidApp {
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 = video.magnet;
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(
@@ -1750,12 +1788,24 @@ class bitvidApp {
this.creatorAvatar.alt = creatorProfile.name;
}
// Cleanup any previous WebTorrent streams, then start a fresh one
// Cleanup any previous WebTorrent streams, then start fresh playback
await torrentClient.cleanup();
const cacheBustedMagnet = video.magnet + "&ts=" + Date.now();
this.log("Starting video stream with:", cacheBustedMagnet);
// Autoplay preferences
if (!hasMagnet) {
if (this.modalPeers) {
this.modalPeers.textContent = "";
}
if (this.modalSpeed) {
this.modalSpeed.textContent = "";
}
if (this.modalDownloaded) {
this.modalDownloaded.textContent = "";
}
if (this.modalProgress) {
this.modalProgress.style.width = "0%";
}
}
const storedUnmuted = localStorage.getItem("unmutedAutoplay");
const userWantsUnmuted = storedUnmuted === "true";
this.modalVideo.muted = !userWantsUnmuted;
@@ -1767,61 +1817,115 @@ class bitvidApp {
);
});
const realTorrent = await torrentClient.streamVideo(
cacheBustedMagnet,
this.modalVideo
);
const attemptAutoplay = () => {
this.modalVideo.play().catch((err) => {
this.log("Autoplay failed:", err);
if (!this.modalVideo.muted) {
this.log("Falling back to muted autoplay.");
this.modalVideo.muted = true;
this.modalVideo.play().catch((err2) => {
this.log("Muted autoplay also failed:", err2);
});
}
});
};
// Try playing; if autoplay fails, fallback to muted
this.modalVideo.play().catch((err) => {
this.log("Autoplay failed:", err);
if (!this.modalVideo.muted) {
this.log("Falling back to muted autoplay.");
this.modalVideo.muted = true;
this.modalVideo.play().catch((err2) => {
this.log("Muted autoplay also failed:", err2);
const setupTorrentMonitors = (torrentInstance) => {
if (!torrentInstance) {
return;
}
const updateInterval = setInterval(() => {
if (!document.body.contains(this.modalVideo)) {
clearInterval(updateInterval);
return;
}
this.updateTorrentStatus(torrentInstance);
}, 3000);
this.activeIntervals.push(updateInterval);
const mirrorInterval = setInterval(() => {
if (!document.body.contains(this.modalVideo)) {
clearInterval(mirrorInterval);
return;
}
const status = document.getElementById("status");
const progress = document.getElementById("progress");
const peers = document.getElementById("peers");
const speed = document.getElementById("speed");
const downloaded = document.getElementById("downloaded");
if (status && this.modalStatus) {
this.modalStatus.textContent = status.textContent;
}
if (progress && this.modalProgress) {
this.modalProgress.style.width = progress.style.width;
}
if (peers && this.modalPeers) {
this.modalPeers.textContent = peers.textContent;
}
if (speed && this.modalSpeed) {
this.modalSpeed.textContent = speed.textContent;
}
if (downloaded && this.modalDownloaded) {
this.modalDownloaded.textContent = downloaded.textContent;
}
}, 3000);
this.activeIntervals.push(mirrorInterval);
};
const startTorrentPlayback = async () => {
if (!hasMagnet) {
return null;
}
const cacheBustedMagnet = `${trimmedMagnet}&ts=${Date.now()}`;
this.log("Starting video stream with:", cacheBustedMagnet);
if (this.modalStatus) {
this.modalStatus.textContent = "Streaming via WebTorrent";
}
const torrentInstance = await torrentClient.streamVideo(
cacheBustedMagnet,
this.modalVideo
);
setupTorrentMonitors(torrentInstance);
return torrentInstance;
};
if (hasUrl) {
if (this.modalStatus) {
this.modalStatus.textContent = "Loading video...";
}
const markUrlReady = () => {
if (this.modalStatus) {
this.modalStatus.textContent = "Streaming from URL";
}
};
this.modalVideo.addEventListener("loadeddata", markUrlReady, {
once: true,
});
this.modalVideo.src = trimmedUrl;
this.modalVideo.load();
if (hasMagnet) {
const fallbackToTorrent = async () => {
this.modalVideo.removeEventListener("error", fallbackToTorrent);
this.log("HTTP playback failed, falling back to WebTorrent.");
try {
await torrentClient.cleanup();
await startTorrentPlayback();
attemptAutoplay();
} catch (torrentErr) {
console.error("WebTorrent fallback failed:", torrentErr);
this.showError("Unable to play this video.");
}
};
this.modalVideo.addEventListener("error", fallbackToTorrent, {
once: true,
});
}
});
} else {
await startTorrentPlayback();
}
// Update torrent stats every 3s
const updateInterval = setInterval(() => {
if (!document.body.contains(this.modalVideo)) {
clearInterval(updateInterval);
return;
}
this.updateTorrentStatus(realTorrent);
}, 3000);
this.activeIntervals.push(updateInterval);
// Mirror stats into the modal if needed
const mirrorInterval = setInterval(() => {
if (!document.body.contains(this.modalVideo)) {
clearInterval(mirrorInterval);
return;
}
const status = document.getElementById("status");
const progress = document.getElementById("progress");
const peers = document.getElementById("peers");
const speed = document.getElementById("speed");
const downloaded = document.getElementById("downloaded");
if (status && this.modalStatus) {
this.modalStatus.textContent = status.textContent;
}
if (progress && this.modalProgress) {
this.modalProgress.style.width = progress.style.width;
}
if (peers && this.modalPeers) {
this.modalPeers.textContent = peers.textContent;
}
if (speed && this.modalSpeed) {
this.modalSpeed.textContent = speed.textContent;
}
if (downloaded && this.modalDownloaded) {
this.modalDownloaded.textContent = downloaded.textContent;
}
}, 3000);
this.activeIntervals.push(mirrorInterval);
attemptAutoplay();
} catch (error) {
this.log("Error in playVideoByEventId:", error);
this.showError(`Playback error: ${error.message}`);
+11 -3
View File
@@ -331,6 +331,9 @@ async function loadUserVideos(pubkey) {
"duration-300"
);
const encodedUrl = encodeURIComponent(video.url || "");
const encodedMagnet = encodeURIComponent(video.magnet || "");
cardEl.innerHTML = `
<div class="cursor-pointer relative group">
<div class="ratio-16-9">
@@ -345,7 +348,9 @@ async function loadUserVideos(pubkey) {
<div>
<h3
class="text-lg font-bold text-white mb-2 line-clamp-2"
data-play-magnet="${encodeURIComponent(video.magnet)}"
data-video-id="${video.id}"
data-play-url="${encodedUrl}"
data-play-magnet="${encodedMagnet}"
>
${safeTitle}
</h3>
@@ -453,13 +458,15 @@ function localConvertEventToVideo(event) {
try {
const content = JSON.parse(event.content || "{}");
const isSupportedVersion = content.version >= 2;
const hasRequiredFields = !!(content.title && content.magnet);
const hasRequiredFields = !!(
content.title && (content.url || content.magnet)
);
if (!isSupportedVersion) {
return { id: event.id, invalid: true, reason: "version <2" };
}
if (!hasRequiredFields) {
return { id: event.id, invalid: true, reason: "missing title/magnet" };
return { id: event.id, invalid: true, reason: "missing title/url+magnet" };
}
return {
@@ -468,6 +475,7 @@ function localConvertEventToVideo(event) {
version: content.version,
isPrivate: content.isPrivate ?? false,
title: content.title ?? "",
url: content.url ?? "",
magnet: content.magnet ?? "",
thumbnail: content.thumbnail ?? "",
description: content.description ?? "",
+28 -8
View File
@@ -53,7 +53,9 @@ function convertEventToVideo(event) {
// Example checks:
const isSupportedVersion = content.version >= 2;
const hasRequiredFields = !!(content.title && content.magnet);
const hasRequiredFields = !!(
content.title && (content.url || content.magnet)
);
if (!isSupportedVersion) {
return {
@@ -66,7 +68,7 @@ function convertEventToVideo(event) {
return {
id: event.id,
invalid: true,
reason: "missing title/magnet",
reason: "missing title/url+magnet",
};
}
@@ -76,6 +78,7 @@ function convertEventToVideo(event) {
version: content.version,
isPrivate: content.isPrivate ?? false,
title: content.title ?? "",
url: content.url ?? "",
magnet: content.magnet ?? "",
thumbnail: content.thumbnail ?? "",
description: content.description ?? "",
@@ -222,10 +225,13 @@ class NostrClient {
console.log("Publishing new video with data:", videoData);
}
let finalMagnet = videoData.magnet;
if (videoData.isPrivate) {
const rawMagnet = typeof videoData.magnet === "string" ? videoData.magnet : "";
let finalMagnet = rawMagnet.trim();
if (videoData.isPrivate && finalMagnet) {
finalMagnet = fakeEncrypt(finalMagnet);
}
const finalUrl =
typeof videoData.url === "string" ? videoData.url.trim() : "";
// brand-new root & d
const videoRootId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
@@ -237,6 +243,7 @@ class NostrClient {
deleted: false,
isPrivate: videoData.isPrivate ?? false,
title: videoData.title || "",
url: finalUrl,
magnet: finalMagnet,
thumbnail: videoData.thumbnail || "",
description: videoData.description || "",
@@ -322,14 +329,23 @@ class NostrClient {
oldPlainMagnet = fakeDecrypt(oldPlainMagnet);
}
const oldUrl = baseEvent.url || "";
// Determine if the updated note should be private
const wantPrivate = updatedData.isPrivate ?? baseEvent.isPrivate ?? false;
// Use the new magnet if provided; otherwise, fall back to the decrypted old magnet
let finalPlainMagnet = (updatedData.magnet || "").trim() || oldPlainMagnet;
let finalMagnet = wantPrivate
? fakeEncrypt(finalPlainMagnet)
: finalPlainMagnet;
const newMagnetValue =
typeof updatedData.magnet === "string" ? updatedData.magnet.trim() : "";
let finalPlainMagnet = newMagnetValue || oldPlainMagnet;
let finalMagnet =
wantPrivate && finalPlainMagnet
? fakeEncrypt(finalPlainMagnet)
: finalPlainMagnet;
const newUrlValue =
typeof updatedData.url === "string" ? updatedData.url.trim() : "";
const finalUrl = newUrlValue || oldUrl;
// Use the existing videoRootId (or fall back to the base event's ID)
const oldRootId = baseEvent.videoRootId || baseEvent.id;
@@ -344,6 +360,7 @@ class NostrClient {
deleted: false,
isPrivate: wantPrivate,
title: updatedData.title ?? baseEvent.title,
url: finalUrl,
magnet: finalMagnet,
thumbnail: updatedData.thumbnail ?? baseEvent.thumbnail,
description: updatedData.description ?? baseEvent.description,
@@ -420,6 +437,7 @@ class NostrClient {
deleted: fetched.deleted,
isPrivate: fetched.isPrivate,
title: fetched.title,
url: fetched.url,
magnet: fetched.magnet,
thumbnail: fetched.thumbnail,
description: fetched.description,
@@ -451,6 +469,7 @@ class NostrClient {
deleted: true,
isPrivate: oldContent.isPrivate ?? false,
title: oldContent.title || "",
url: "",
magnet: "",
thumbnail: "",
description: "This version was reverted by the creator.",
@@ -530,6 +549,7 @@ class NostrClient {
deleted: vid.deleted,
isPrivate: vid.isPrivate,
title: vid.title,
url: vid.url,
magnet: vid.magnet,
thumbnail: vid.thumbnail,
description: vid.description,
+12 -3
View File
@@ -329,11 +329,15 @@ 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 cardHtml = `
<div class="video-card bg-gray-900 rounded-lg overflow-hidden shadow-lg hover:shadow-2xl transition-all duration-300 ${highlightClass}">
<a
href="${shareUrl}"
data-play-magnet="${encodeURIComponent(video.magnet)}"
data-video-id="${video.id}"
data-play-url="${encodedUrl}"
data-play-magnet="${encodedMagnet}"
class="block cursor-pointer relative group"
>
<div class="ratio-16-9">
@@ -347,7 +351,9 @@ class SubscriptionsManager {
<div class="p-4">
<h3
class="text-lg font-bold text-white line-clamp-2 hover:text-blue-400 cursor-pointer mb-3"
data-play-magnet="${encodeURIComponent(video.magnet)}"
data-video-id="${video.id}"
data-play-url="${encodedUrl}"
data-play-magnet="${encodedMagnet}"
>
${safeTitle}
</h3>
@@ -482,7 +488,9 @@ class SubscriptionsManager {
convertEventToVideo(evt) {
try {
const content = JSON.parse(evt.content || "{}");
const hasFields = !!(content.title && content.magnet);
const hasFields = !!(
content.title && (content.url || content.magnet)
);
const versionOk = content.version >= 2;
if (!versionOk || !hasFields) {
return { id: evt.id, invalid: true };
@@ -496,6 +504,7 @@ class SubscriptionsManager {
deleted: content.deleted === true,
isPrivate: content.isPrivate === true,
title: content.title || "",
url: content.url || "",
magnet: content.magnet || "",
thumbnail: content.thumbnail || "",
description: content.description || "",