Handle WebTorrent fallback without service worker

This commit is contained in:
thePR0M3TH3AN committed 2025-10-15 14:54:24 -04:00
1 parent e23a2c462d
commit 78ce75ef62
4 files changed
+198 -18

No files matched your search

+19
View File
@@ -81,6 +81,7 @@ import { pointerArrayToKey } from "./utils/pointer.js";
import { resolveVideoPointer } from "./utils/videoPointer.js";
import { isValidMagnetUri } from "./utils/magnetValidators.js";
import { dedupeToNewestByRoot } from "./utils/videoDeduper.js";
import { buildServiceWorkerFallbackStatus } from "./utils/serviceWorkerFallbackMessages.js";
import {
getVideoRootIdentifier,
applyRootTimestampToVideosMap,
@@ -5635,6 +5636,24 @@ class Application {
this.modalVideo,
{ urlList: sanitizedUrlList }
);
if (torrentClient.isServiceWorkerUnavailable()) {
const swError = torrentClient.getServiceWorkerInitError();
const statusMessage = buildServiceWorkerFallbackStatus(swError);
this.log(
"[playViaWebTorrent] Service worker unavailable; streaming directly via WebTorrent.",
swError
);
if (swError) {
userLogger.warn(
"[playViaWebTorrent] Service worker unavailable; direct streaming engaged.",
swError
);
}
if (this.videoModal) {
this.videoModal.updateStatus(statusMessage);
}
}
if (torrentInstance && torrentInstance.ready) {
// Some browsers delay `playing` events for MediaSource-backed torrents.
// Clearing the poster here prevents the historic "GIF stuck over the
+58
View File
@@ -0,0 +1,58 @@
// js/utils/serviceWorkerFallbackMessages.js
const BASE_STATUS_MESSAGE = "Streaming via WebTorrent";
function normalizeMessage(error) {
if (!error) {
return "";
}
if (error instanceof Error && typeof error.message === "string") {
return error.message;
}
if (typeof error === "string") {
return error;
}
try {
return String(error);
} catch (_err) {
return "";
}
}
export function buildServiceWorkerFallbackStatus(error) {
const rawMessage = normalizeMessage(error);
const normalized = rawMessage.toLowerCase();
if (!normalized) {
return `${BASE_STATUS_MESSAGE} (service worker unavailable)`;
}
if (normalized.includes("https or localhost required")) {
return `${BASE_STATUS_MESSAGE} (serve over HTTPS to enable service worker)`;
}
if (normalized.includes("not supported") || normalized.includes("disabled")) {
return `${BASE_STATUS_MESSAGE} (browser disabled service workers)`;
}
if (normalized.includes("brave shield")) {
return `${BASE_STATUS_MESSAGE} (Brave Shields blocked the service worker)`;
}
if (
normalized.includes("failed to register") ||
normalized.includes("failed to fetch") ||
normalized.includes("blocked") ||
normalized.includes("net::err_blocked_by_client")
) {
return `${BASE_STATUS_MESSAGE} (service worker blocked by browser or extension)`;
}
if (normalized.includes("controller claim timeout")) {
return `${BASE_STATUS_MESSAGE} (waiting for service worker control)`;
}
return `${BASE_STATUS_MESSAGE} (service worker unavailable)`;
}
export default buildServiceWorkerFallbackStatus;
+64 -18
View File
@@ -125,7 +125,9 @@ function toError(err) {
}
export class TorrentClient {
constructor() {
constructor({ webTorrentClass } = {}) {
this.WebTorrentClass =
typeof webTorrentClass === "function" ? webTorrentClass : WebTorrent;
// Reusable objects and flags
this.client = null;
this.currentTorrent = null;
@@ -135,13 +137,16 @@ export class TorrentClient {
this.swRegistration = null;
this.serverCreated = false; // Indicates if we've called createServer on this.client
this.serviceWorkerDisabled = false;
this.serviceWorkerError = null;
// Timeout for SW operations
this.TIMEOUT_DURATION = 60000;
}
ensureClientForProbe() {
if (!this.probeClient) {
this.probeClient = new WebTorrent();
this.probeClient = new this.WebTorrentClass();
}
return this.probeClient;
}
@@ -288,8 +293,16 @@ export class TorrentClient {
});
}
log(msg) {
devLogger.log(msg);
log(...args) {
devLogger.log(...args);
}
isServiceWorkerUnavailable() {
return this.serviceWorkerDisabled;
}
getServiceWorkerInitError() {
return this.serviceWorkerError || null;
}
async isBrave() {
@@ -307,21 +320,52 @@ export class TorrentClient {
* Called once from streamVideo.
*/
async init() {
// 1) If the client doesn't exist, create it
if (!this.client) {
this.client = new WebTorrent();
this.client = new this.WebTorrentClass();
}
// 2) If we haven’t registered the service worker yet, do it now
if (!this.swRegistration) {
this.swRegistration = await this.setupServiceWorker();
} else {
// Even with an existing registration we still wait for control so that a
// transient controller drop (e.g. Chrome devtools unregister/reload) does
// not resurrect the grey-screen regression mentioned in
// waitForActiveController().
this.requestClientsClaim(this.swRegistration);
await this.waitForActiveController(this.swRegistration);
if (this.serviceWorkerDisabled) {
return {
serviceWorkerReady: false,
registration: null,
error: this.serviceWorkerError,
};
}
try {
if (!this.swRegistration) {
this.swRegistration = await this.setupServiceWorker();
} else {
// Even with an existing registration we still wait for control so that a
// transient controller drop (e.g. Chrome devtools unregister/reload) does
// not resurrect the grey-screen regression mentioned in
// waitForActiveController().
this.requestClientsClaim(this.swRegistration);
await this.waitForActiveController(this.swRegistration);
}
return {
serviceWorkerReady: !!this.swRegistration,
registration: this.swRegistration,
};
} catch (error) {
const normalizedError = toError(error);
this.log(
"[WebTorrent] Service worker setup failed; continuing without it:",
normalizedError
);
userLogger.warn(
"[WebTorrent] Service worker unavailable; falling back to direct streaming.",
normalizedError
);
this.serviceWorkerDisabled = true;
this.serviceWorkerError = normalizedError;
this.swRegistration = null;
return {
serviceWorkerReady: false,
registration: null,
error: normalizedError,
};
}
}
@@ -777,10 +821,12 @@ export class TorrentClient {
async streamVideo(magnetURI, videoElement, opts = {}) {
try {
// 1) Make sure we have a WebTorrent client and a valid SW registration.
await this.init();
const initResult = await this.init();
const serviceWorkerReady =
!!(initResult?.serviceWorkerReady && this.swRegistration);
// 2) Create the server once if not already created.
if (!this.serverCreated) {
if (serviceWorkerReady && !this.serverCreated) {
this.client.createServer({
controller: this.swRegistration,
pathPrefix: location.origin + "/webtorrent",
@@ -0,0 +1,57 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
buildServiceWorkerFallbackStatus,
} from "../../js/utils/serviceWorkerFallbackMessages.js";
const BASE = "Streaming via WebTorrent";
test("returns generic message when error missing", () => {
assert.strictEqual(
buildServiceWorkerFallbackStatus(null),
`${BASE} (service worker unavailable)`
);
});
test("identifies HTTPS requirement errors", () => {
const error = new Error("HTTPS or localhost required");
assert.strictEqual(
buildServiceWorkerFallbackStatus(error),
`${BASE} (serve over HTTPS to enable service worker)`
);
});
test("identifies disabled service worker errors", () => {
const error = new Error("Service Worker not supported or disabled");
assert.strictEqual(
buildServiceWorkerFallbackStatus(error),
`${BASE} (browser disabled service workers)`
);
});
test("identifies Brave specific guidance", () => {
const error = new Error("Please enable Service Workers in Brave Shield settings");
assert.strictEqual(
buildServiceWorkerFallbackStatus(error),
`${BASE} (Brave Shields blocked the service worker)`
);
});
test("identifies blocked script errors", () => {
const error = new Error(
"Failed to register ServiceWorker: TypeError: Failed to fetch"
);
assert.strictEqual(
buildServiceWorkerFallbackStatus(error),
`${BASE} (service worker blocked by browser or extension)`
);
});
test("identifies controller claim timeout", () => {
const error = new Error("Service worker controller claim timeout");
assert.strictEqual(
buildServiceWorkerFallbackStatus(error),
`${BASE} (waiting for service worker control)`
);
});