mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-29 08:52:25 +00:00
Support URL-only videos and update playback fallback
This commit is contained in:
1 parent
7350f43ca7
commit
9014371263
4 files changed
+219
-78
No files matched your search
@@ -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
@@ -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
@@ -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
@@ -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 || "",
|
||||
|
||||
Reference in new issue
Block a user