This commit is contained in:
2025-02-03 19:36:30 -05:00
parent 00cd669b61
commit 139e279e35
2 changed files with 68 additions and 79 deletions

View File

@@ -41,7 +41,7 @@ export class TorrentClient {
return false; return false;
}; };
// If it's already active // If it's already active, resolve immediately
if (checkActivation()) return; if (checkActivation()) return;
registration.addEventListener("activate", () => { registration.addEventListener("activate", () => {
@@ -88,11 +88,11 @@ export class TorrentClient {
for (const reg of registrations) { for (const reg of registrations) {
await reg.unregister(); await reg.unregister();
} }
// A short wait to ensure old workers are gone // Short delay to ensure old workers are removed
await new Promise((resolve) => setTimeout(resolve, 1000)); await new Promise((resolve) => setTimeout(resolve, 1000));
} }
// Register sw.min.js at the root path "/sw.min.js", with scope "/" // Register sw.min.js from the root (Netlify serves it at /sw.min.js)
this.log("Registering service worker at /sw.min.js..."); this.log("Registering service worker at /sw.min.js...");
const registration = await navigator.serviceWorker.register("/sw.min.js", { const registration = await navigator.serviceWorker.register("/sw.min.js", {
scope: "/", scope: "/",
@@ -123,7 +123,7 @@ export class TorrentClient {
await this.waitForServiceWorkerActivation(registration); await this.waitForServiceWorkerActivation(registration);
this.log("Service worker activated"); this.log("Service worker activated");
// Double-check the SW is fully ready // Ensure the service worker is fully ready
const readyRegistration = await Promise.race([ const readyRegistration = await Promise.race([
navigator.serviceWorker.ready, navigator.serviceWorker.ready,
new Promise((_, reject) => new Promise((_, reject) =>
@@ -148,7 +148,6 @@ export class TorrentClient {
// Minimal handleChromeTorrent // Minimal handleChromeTorrent
handleChromeTorrent(torrent, videoElement, resolve, reject) { handleChromeTorrent(torrent, videoElement, resolve, reject) {
// Listen for warnings, e.g. potential CORS blocks
torrent.on("warning", (err) => { torrent.on("warning", (err) => {
if (err && typeof err.message === "string") { if (err && typeof err.message === "string") {
if ( if (
@@ -179,16 +178,13 @@ export class TorrentClient {
return reject(new Error("No compatible video file found in torrent")); return reject(new Error("No compatible video file found in torrent"));
} }
// Mute & cross-origin
videoElement.muted = true; videoElement.muted = true;
videoElement.crossOrigin = "anonymous"; videoElement.crossOrigin = "anonymous";
// Handle video-level errors
videoElement.addEventListener("error", (e) => { videoElement.addEventListener("error", (e) => {
this.log("Video error:", e.target.error); this.log("Video error:", e.target.error);
}); });
// Attempt autoplay when canplay
videoElement.addEventListener("canplay", () => { videoElement.addEventListener("canplay", () => {
videoElement.play().catch((err) => { videoElement.play().catch((err) => {
this.log("Autoplay failed:", err); this.log("Autoplay failed:", err);
@@ -249,7 +245,7 @@ export class TorrentClient {
/** /**
* Initiates streaming of a torrent magnet to a <video> element. * Initiates streaming of a torrent magnet to a <video> element.
* Use `setupServiceWorker()` first to ensure the SW is registered. * Ensures the service worker is registered first.
*/ */
async streamVideo(magnetURI, videoElement) { async streamVideo(magnetURI, videoElement) {
try { try {
@@ -259,13 +255,9 @@ export class TorrentClient {
throw new Error("Service worker setup failed"); throw new Error("Service worker setup failed");
} }
// 2) Optionally configure a pathPrefix here if your SW // Create the WebTorrent server with the registered service worker.
// intercepts /webtorrent/ or /src/webtorrent // (If you need to specify a custom URL prefix for torrent streaming,
// this.client.createServer({ // pass a pathPrefix option here.)
// controller: registration,
// pathPrefix: "/webtorrent",
// });
this.client.createServer({ controller: registration }); this.client.createServer({ controller: registration });
this.log("WebTorrent server created"); this.log("WebTorrent server created");
@@ -297,7 +289,7 @@ export class TorrentClient {
} }
/** /**
* Clean up resources * Clean up resources.
*/ */
async cleanup() { async cleanup() {
try { try {

117
src/sw.min.js vendored
View File

@@ -15,15 +15,13 @@
self.skipWaiting(); self.skipWaiting();
}); });
// Claim clients on activation // Claim clients on activation and clear caches
self.addEventListener("activate", (event) => { self.addEventListener("activate", (event) => {
event.waitUntil( event.waitUntil(
Promise.all([ Promise.all([
clients.claim(), clients.claim(),
self.skipWaiting(), self.skipWaiting(),
caches caches.keys().then((cacheNames) =>
.keys()
.then((cacheNames) =>
Promise.all(cacheNames.map((cacheName) => caches.delete(cacheName))) Promise.all(cacheNames.map((cacheName) => caches.delete(cacheName)))
), ),
]) ])
@@ -31,22 +29,22 @@
}); });
// Handle fetch events // Handle fetch events
self.addEventListener("fetch", (s) => { self.addEventListener("fetch", (event) => {
const t = ((s) => { const responsePromise = (() => {
const { url: t } = s.request; const requestURL = event.request.url;
// Only handle WebTorrent streaming requests
// Only handle webtorrent requests // Since our SW is registered with scope "/" the expected URL prefix is "/webtorrent/"
if (!t.includes(self.registration.scope + "webtorrent/")) { if (!requestURL.includes("/webtorrent/")) {
return null; return null;
} }
// Handle keepalive requests // Handle keepalive requests
if (t.includes(self.registration.scope + "webtorrent/keepalive/")) { if (requestURL.includes("/webtorrent/keepalive/")) {
return new Response(); return new Response();
} }
// Handle cancel requests // Handle cancel requests
if (t.includes(self.registration.scope + "webtorrent/cancel/")) { if (requestURL.includes("/webtorrent/cancel/")) {
return new Response( return new Response(
new ReadableStream({ new ReadableStream({
cancel() { cancel() {
@@ -57,87 +55,86 @@
} }
// Handle streaming requests // Handle streaming requests
return (async function ({ request: s }) { return (async function ({ request }) {
const { url: t, method: n, headers: o, destination: a } = s; const { url, method, headers, destination } = request;
// Get all window clients // Get all window clients
const l = await clients.matchAll({ const windowClients = await clients.matchAll({
type: "window", type: "window",
includeUncontrolled: true, includeUncontrolled: true,
}); });
// Create message channel and wait for response // Create a message channel and wait for a response from a client
const [r, i] = await new Promise((e) => { const [clientResponse, port] = await new Promise((resolve) => {
for (const s of l) { for (const client of windowClients) {
const l = new MessageChannel(), const channel = new MessageChannel();
{ port1: r, port2: i } = l; channel.port1.onmessage = ({ data }) => {
r.onmessage = ({ data: s }) => { resolve([data, channel.port1]);
e([s, r]);
}; };
s.postMessage( client.postMessage(
{ {
url: t, url,
method: n, method,
headers: Object.fromEntries(o.entries()), headers: Object.fromEntries(headers.entries()),
scope: self.registration.scope, scope: self.registration.scope,
destination: a, destination,
type: "webtorrent", type: "webtorrent",
}, },
[i] [channel.port2]
); );
} }
}); });
let c = null; let timeoutId = null;
const closeChannel = () => {
const d = () => { port.postMessage(false);
i.postMessage(false); clearTimeout(timeoutId);
clearTimeout(c); port.onmessage = null;
i.onmessage = null;
}; };
// Handle non-streaming response // If the response is not a streaming request, return a normal response
if (r.body !== "STREAM") { if (clientResponse.body !== "STREAM") {
d(); closeChannel();
return new Response(r.body, r); return new Response(clientResponse.body, clientResponse);
} }
// Handle streaming response // Otherwise, handle streaming response using a ReadableStream
return new Response( return new Response(
new ReadableStream({ new ReadableStream({
pull: (s) => pull(controller) {
new Promise((t) => { return new Promise((resolvePull) => {
i.onmessage = ({ data: e }) => { port.onmessage = ({ data }) => {
if (e) { if (data) {
s.enqueue(e); controller.enqueue(data);
} else { } else {
d(); closeChannel();
s.close(); controller.close();
} }
t(); resolvePull();
}; };
if (!cancelled && a !== "document") { if (!cancelled && destination !== "document") {
clearTimeout(c); clearTimeout(timeoutId);
c = setTimeout(() => { timeoutId = setTimeout(() => {
d(); closeChannel();
t(); resolvePull();
}, 5000); }, 5000);
} }
i.postMessage(true); port.postMessage(true);
}), });
},
cancel() { cancel() {
d(); closeChannel();
}, },
}), }),
r clientResponse
); );
})(s); })(event);
})(s); })();
if (t) { if (responsePromise) {
s.respondWith(t); event.respondWith(responsePromise);
} }
}); });
})(); })();