diff --git a/src/js/app.js b/src/js/app.js index 60d2802..6c14793 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -700,44 +700,46 @@ class bitvidApp { async loadVideos() { console.log("Starting loadVideos..."); - // 1) If there's an existing subscription, unsubscribe it - if (this.videoSubscription) { - this.videoSubscription.unsub(); - this.videoSubscription = null; - } - - // 2) Show "Loading..." message - if (this.videoList) { - this.videoList.innerHTML = ` -
- Loading videos... -
`; - } - - try { - // 3) Force a bulk fetch - await nostrClient.fetchVideos(); - - // 4) Instead of reusing the entire fetched array, - // use getActiveVideos() for the final display: - const newestActive = nostrClient.getActiveVideos(); - this.renderVideoList(newestActive); - - // 5) Subscribe for updates - this.videoSubscription = nostrClient.subscribeVideos((video) => { - // Whenever we get a new or updated event, re-render the newest set: - const activeAll = nostrClient.getActiveVideos(); - this.renderVideoList(activeAll); - }); - } catch (err) { - console.error("Could not load videos:", err); - this.showError("Could not load videos from relays."); + // If we already have a subscription, don’t unsubscribe/resubscribe— + // just update the UI from local cache. + if (!this.videoSubscription) { + // First-time load: show “Loading...” message if (this.videoList) { this.videoList.innerHTML = `- No videos available at this time. + Loading videos...
`; } + + // 1) Do a bulk fetch once + try { + await nostrClient.fetchVideos(); + } catch (err) { + console.error("Could not load videos initially:", err); + this.showError("Could not load videos from relays."); + if (this.videoList) { + this.videoList.innerHTML = ` ++ No videos available at this time. +
`; + } + return; + } + + // 2) Render the newest set after the fetch + const newestActive = nostrClient.getActiveVideos(); + this.renderVideoList(newestActive); + + // 3) Create a single subscription that updates our UI + this.videoSubscription = nostrClient.subscribeVideos(() => { + // Each time a new/updated event arrives, we just re-render from local + const updatedAll = nostrClient.getActiveVideos(); + this.renderVideoList(updatedAll); + }); + } else { + // If we’ve already subscribed before, just update from cache + const allCached = nostrClient.getActiveVideos(); + this.renderVideoList(allCached); } } @@ -1017,26 +1019,27 @@ class bitvidApp { return; } - // Look up the video in our subscription map + // 1) Check local 'videosMap' or 'nostrClient.getActiveVideos()' let matchedVideo = Array.from(this.videosMap.values()).find( (v) => v.magnet === decodedMagnet ); - - // If not found in the map, do a fallback fetch if (!matchedVideo) { - const allVideos = await nostrClient.fetchVideos(); - matchedVideo = allVideos.find((v) => v.magnet === decodedMagnet); + // Instead of forcing a full `fetchVideos()`, + // try looking in the activeVideos from local cache: + const activeVideos = nostrClient.getActiveVideos(); + matchedVideo = activeVideos.find((v) => v.magnet === decodedMagnet); } + // If still not found, you can do a single event-based approach or just show an error: if (!matchedVideo) { - this.showError("No matching video found."); + this.showError("No matching video found in local cache."); return; } - // Update our tracking + // Update tracking this.currentMagnetUri = decodedMagnet; - // Hand off to the method that already sets modal fields and streams + // Delegate to the main method await this.playVideoByEventId(matchedVideo.id); } catch (error) { console.error("Error in playVideo:", error); @@ -1465,27 +1468,31 @@ class bitvidApp { return video; } - // 2) Bulk fetch from relays - const allFromBulk = await nostrClient.fetchVideos(); - - // 2a) Deduplicate so we only keep newest version per root - const newestPerRoot = dedupeToNewestByRoot(allFromBulk); - - // 2b) Find the requested ID within the deduplicated set - video = newestPerRoot.find((v) => v.id === eventId); - if (video) { - // Store it in our local map, so we can open it instantly next time - this.videosMap.set(video.id, video); - return video; + // 2) Already in nostrClient.allEvents? + // (assuming nostrClient.allEvents is a Map of id => video) + const fromAll = nostrClient.allEvents.get(eventId); + if (fromAll && !fromAll.deleted) { + this.videosMap.set(eventId, fromAll); + return fromAll; } - // 3) Final fallback: direct single-event fetch + // 3) Direct single-event fetch (fewer resources than full fetchVideos) const single = await nostrClient.getEventById(eventId); if (single && !single.deleted) { this.videosMap.set(single.id, single); return single; } + // 4) If you wanted a final fallback, you could do it here: + // But it's typically better to avoid repeated full fetches + // console.log("Falling back to full fetchVideos..."); + // const allFetched = await nostrClient.fetchVideos(); + // video = allFetched.find(v => v.id === eventId && !v.deleted); + // if (video) { + // this.videosMap.set(video.id, video); + // return video; + // } + // Not found or was deleted return null; } diff --git a/src/js/webtorrent.js b/src/js/webtorrent.js index e4db9ae..964a1b2 100644 --- a/src/js/webtorrent.js +++ b/src/js/webtorrent.js @@ -7,8 +7,6 @@ export class TorrentClient { this.client = new WebTorrent(); this.currentTorrent = null; this.TIMEOUT_DURATION = 60000; // 60 seconds - // We remove the “statsInterval” since we’re not using it here anymore - // this.statsInterval = null; } log(msg) { @@ -43,6 +41,7 @@ export class TorrentClient { return false; }; + // Check if already active if (checkActivation()) return; registration.addEventListener("activate", () => { @@ -60,6 +59,9 @@ export class TorrentClient { }); } + // -------------------------- + // UPDATED: setupServiceWorker + // -------------------------- async setupServiceWorker() { try { const isBraveBrowser = await this.isBrave(); @@ -83,6 +85,7 @@ export class TorrentClient { throw new Error("Please enable WebRTC in Brave Shield settings"); } + // Unregister any existing SW const registrations = await navigator.serviceWorker.getRegistrations(); for (const registration of registrations) { await registration.unregister(); @@ -90,17 +93,13 @@ export class TorrentClient { await new Promise((resolve) => setTimeout(resolve, 1000)); } - const currentPath = window.location.pathname; - const basePath = currentPath.substring( - 0, - currentPath.lastIndexOf("/") + 1 - ); - - this.log("Registering service worker..."); + // Directly register sw.min.js at /src/sw.min.js + // with a scope that covers /src/ (which includes /src/webtorrent). + this.log("Registering service worker at /src/sw.min.js..."); const registration = await navigator.serviceWorker.register( - "./sw.min.js", + "/src/sw.min.js", { - scope: basePath, + scope: "/src/", updateViaCache: "none", } ); @@ -129,6 +128,7 @@ export class TorrentClient { await this.waitForServiceWorkerActivation(registration); this.log("Service worker activated"); + // Make sure the SW is fully ready const readyRegistration = await Promise.race([ navigator.serviceWorker.ready, new Promise((_, reject) => @@ -171,8 +171,14 @@ export class TorrentClient { throw new Error("Service worker setup failed"); } - // 2) Create WebTorrent server - this.client.createServer({ controller: registration }); + // ------------------------------------------------ + // UPDATED: Pass pathPrefix to createServer + // so that it uses /src/webtorrent/... for streaming + // ------------------------------------------------ + this.client.createServer({ + controller: registration, + pathPrefix: "/src/webtorrent", + }); this.log("WebTorrent server created"); const isFirefoxBrowser = this.isFirefox(); @@ -202,11 +208,37 @@ export class TorrentClient { } } - // Minimal handleChromeTorrent — no internal setInterval + // Minimal handleChromeTorrent handleChromeTorrent(torrent, videoElement, resolve, reject) { - const file = torrent.files.find((f) => - /\.(mp4|webm|mkv)$/.test(f.name.toLowerCase()) - ); + // OPTIONAL: Listen for “warning” events + torrent.on("warning", (err) => { + if (err && typeof err.message === "string") { + if ( + err.message.includes("CORS") || + err.message.includes("Access-Control-Allow-Origin") + ) { + console.warn( + "CORS warning detected. Attempting to remove the failing webseed/tracker." + ); + // Example cleanup approach... + if (torrent._opts?.urlList?.length) { + torrent._opts.urlList = torrent._opts.urlList.filter((url) => { + return !url.includes("distribution.bbb3d.renderfarming.net"); + }); + console.warn("Cleaned up webseeds =>", torrent._opts.urlList); + } + if (torrent._opts?.announce?.length) { + torrent._opts.announce = torrent._opts.announce.filter((url) => { + return !url.includes("fastcast.nz"); + }); + console.warn("Cleaned up trackers =>", torrent._opts.announce); + } + } + } + }); + + // Then proceed with normal file selection + const file = torrent.files.find((f) => /\.(mp4|webm|mkv)$/i.test(f.name)); if (!file) { return reject(new Error("No compatible video file found in torrent")); } @@ -215,7 +247,7 @@ export class TorrentClient { videoElement.muted = true; videoElement.crossOrigin = "anonymous"; - // Catch video errors + // Catch video-level errors videoElement.addEventListener("error", (e) => { this.log("Video error:", e.target.error); }); @@ -227,7 +259,6 @@ export class TorrentClient { }); }); - // Actually stream try { file.streamTo(videoElement); this.currentTorrent = torrent; @@ -237,7 +268,6 @@ export class TorrentClient { reject(err); } - // Also handle torrent error events torrent.on("error", (err) => { this.log("Torrent error (Chrome path):", err); reject(err); @@ -286,7 +316,6 @@ export class TorrentClient { */ async cleanup() { try { - // No local interval to clear here if (this.currentTorrent) { this.currentTorrent.destroy(); }