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

121
src/sw.min.js vendored
View File

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