greatly improved playback speed on firefox based browsers

This commit is contained in:
Keep Creating Online
2025-01-25 09:10:59 -05:00
parent f55997a9e0
commit a1ff0d2a92
3 changed files with 478 additions and 261 deletions

2
.gitignore vendored
View File

@@ -1 +1,3 @@
saved_config.yaml saved_config.yaml
repo-context.txt
src/webtorrent-docs/

View File

@@ -271,8 +271,10 @@
</svg> </svg>
<p class="text-yellow-200"> <p class="text-yellow-200">
This platform is currently in development and only supports This platform is currently in development and only supports
Chrome-based browsers. Other browsers are not supported at this Chrome and Firefox-based browsers. Other browsers are not
time. You may encounter bugs or missing features. supported at this time. You may encounter bugs or missing
features. Give it a sec. Videos might take 10 to 60 seconds to
load initially.
</p> </p>
</div> </div>

View File

@@ -1,286 +1,499 @@
// <!-- keep this <ai_context> section if it already exists at the top of your file -->
// js/webtorrent.js // js/webtorrent.js
import WebTorrent from './webtorrent.min.js' import WebTorrent from "./webtorrent.min.js";
export class TorrentClient { export class TorrentClient {
constructor() { constructor() {
this.client = new WebTorrent() // Create WebTorrent client
this.currentTorrent = null this.client = new WebTorrent();
this.TIMEOUT_DURATION = 60000 // 60 seconds this.currentTorrent = null;
this.statsInterval = null this.TIMEOUT_DURATION = 60000; // 60 seconds
this.statsInterval = null;
} }
log(msg) { log(msg) {
console.log(msg) console.log(msg);
} }
async isBrave() { async isBrave() {
return (navigator.brave?.isBrave && await navigator.brave.isBrave()) || false return (
(navigator.brave?.isBrave && (await navigator.brave.isBrave())) || false
);
}
isFirefox() {
return /firefox/i.test(window.navigator.userAgent);
} }
async waitForServiceWorkerActivation(registration) { async waitForServiceWorkerActivation(registration) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
reject(new Error('Service worker activation timeout')) reject(new Error("Service worker activation timeout"));
}, this.TIMEOUT_DURATION) }, this.TIMEOUT_DURATION);
this.log('Waiting for service worker activation...') this.log("Waiting for service worker activation...");
const checkActivation = () => { const checkActivation = () => {
if (registration.active) { if (registration.active) {
clearTimeout(timeout) clearTimeout(timeout);
this.log('Service worker is active') this.log("Service worker is active");
resolve(registration) resolve(registration);
return true return true;
}
return false
} }
return false;
};
if (checkActivation()) return if (checkActivation()) return;
registration.addEventListener('activate', () => { registration.addEventListener("activate", () => {
checkActivation() checkActivation();
}) });
if (registration.waiting) { if (registration.waiting) {
this.log('Service worker is waiting, sending skip waiting message') this.log("Service worker is waiting, sending skip waiting message");
registration.waiting.postMessage({ type: 'SKIP_WAITING' }) registration.waiting.postMessage({ type: "SKIP_WAITING" });
} }
registration.addEventListener('statechange', () => { registration.addEventListener("statechange", () => {
checkActivation() checkActivation();
}) });
}) });
} }
/**
* Registers the service worker, waiting until it's fully active before proceeding.
*/
async setupServiceWorker() { async setupServiceWorker() {
try { try {
const isBraveBrowser = await this.isBrave() const isBraveBrowser = await this.isBrave();
if (!window.isSecureContext) { if (!window.isSecureContext) {
throw new Error('HTTPS or localhost required') throw new Error("HTTPS or localhost required");
} }
if (!('serviceWorker' in navigator) || !navigator.serviceWorker) { if (!("serviceWorker" in navigator) || !navigator.serviceWorker) {
throw new Error('Service Worker not supported or disabled') throw new Error("Service Worker not supported or disabled");
} }
// If Brave, we optionally clear all service workers so we can re-register cleanly
if (isBraveBrowser) { if (isBraveBrowser) {
this.log('Checking Brave configuration...') this.log("Checking Brave configuration...");
if (!navigator.serviceWorker) { if (!navigator.serviceWorker) {
throw new Error('Please enable Service Workers in Brave Shield settings') throw new Error(
"Please enable Service Workers in Brave Shield settings"
);
} }
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
throw new Error('Please enable WebRTC in Brave Shield settings') throw new Error("Please enable WebRTC in Brave Shield settings");
} }
const registrations = await navigator.serviceWorker.getRegistrations() const registrations = await navigator.serviceWorker.getRegistrations();
for (const registration of registrations) { for (const registration of registrations) {
await registration.unregister() await registration.unregister();
} }
await new Promise(resolve => setTimeout(resolve, 1000)) await new Promise((resolve) => setTimeout(resolve, 1000));
} }
const currentPath = window.location.pathname const currentPath = window.location.pathname;
const basePath = currentPath.substring(0, currentPath.lastIndexOf('/') + 1) const basePath = currentPath.substring(
0,
currentPath.lastIndexOf("/") + 1
);
this.log('Registering service worker...') this.log("Registering service worker...");
const registration = await navigator.serviceWorker.register('./sw.min.js', { const registration = await navigator.serviceWorker.register(
"./sw.min.js",
{
scope: basePath, scope: basePath,
updateViaCache: 'none' updateViaCache: "none",
}) }
this.log('Service worker registered') );
this.log("Service worker registered");
if (registration.installing) { if (registration.installing) {
this.log('Waiting for installation...') this.log("Waiting for installation...");
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
reject(new Error('Installation timeout')) reject(new Error("Installation timeout"));
}, this.TIMEOUT_DURATION) }, this.TIMEOUT_DURATION);
registration.installing.addEventListener('statechange', (e) => { registration.installing.addEventListener("statechange", (e) => {
this.log('Service worker state:', e.target.state) this.log("Service worker state:", e.target.state);
if (e.target.state === 'activated' || e.target.state === 'redundant') { if (
clearTimeout(timeout) e.target.state === "activated" ||
resolve() e.target.state === "redundant"
) {
clearTimeout(timeout);
resolve();
} }
}) });
}) });
} }
await this.waitForServiceWorkerActivation(registration) // Wait for service worker to become active
this.log('Service worker activated') await this.waitForServiceWorkerActivation(registration);
this.log("Service worker activated");
// Make sure its truly active
const readyRegistration = await Promise.race([ const readyRegistration = await Promise.race([
navigator.serviceWorker.ready, navigator.serviceWorker.ready,
new Promise((_, reject) => new Promise((_, reject) =>
setTimeout(() => reject(new Error('Service worker ready timeout')), this.TIMEOUT_DURATION) setTimeout(
() => reject(new Error("Service worker ready timeout")),
this.TIMEOUT_DURATION
) )
]) ),
]);
if (!readyRegistration.active) { if (!readyRegistration.active) {
throw new Error('Service worker not active after ready state') throw new Error("Service worker not active after ready state");
} }
this.log('Service worker ready') this.log("Service worker ready");
return registration return registration;
} catch (error) { } catch (error) {
this.log('Service worker setup error:', error) this.log("Service worker setup error:", error);
throw error throw error;
} }
} }
formatBytes(bytes) { formatBytes(bytes) {
if (bytes === 0) return '0 B' if (bytes === 0) return "0 B";
const k = 1024 const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'] const sizes = ["B", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k)) const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}` return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`;
} }
/**
* Streams the given magnet URI to the specified <video> element.
*/
async streamVideo(magnetURI, videoElement) { async streamVideo(magnetURI, videoElement) {
try { try {
// Setup service worker first // 1) Setup service worker
const registration = await this.setupServiceWorker() const registration = await this.setupServiceWorker();
if (!registration || !registration.active) { if (!registration || !registration.active) {
throw new Error('Service worker setup failed') throw new Error("Service worker setup failed");
} }
// Create WebTorrent server AFTER service worker is ready // 2) Create WebTorrent server AFTER service worker is ready
this.client.createServer({ controller: registration }) this.client.createServer({ controller: registration });
this.log('WebTorrent server created') this.log("WebTorrent server created");
const isFirefoxBrowser = this.isFirefox();
if (isFirefoxBrowser) {
// ----------------------
// FIREFOX CODE PATH
// (sequential, concurrency limit, smaller chunk)
// ----------------------
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.log('Starting torrent download') this.log("Starting torrent download (Firefox path)");
this.client.add(magnetURI, torrent => { this.client.add(
this.log('Torrent added: ' + torrent.name) magnetURI,
const status = document.getElementById('status') {
const progress = document.getElementById('progress') strategy: "sequential",
const peers = document.getElementById('peers') maxWebConns: 4, // reduce concurrency
const speed = document.getElementById('speed') },
const downloaded = document.getElementById('downloaded') (torrent) => {
this.handleFirefoxTorrent(torrent, videoElement, resolve, reject);
if (status) status.textContent = `Loading ${torrent.name}...` }
);
const file = torrent.files.find(file => });
file.name.endsWith('.mp4') || } else {
file.name.endsWith('.webm') || // ----------------------
file.name.endsWith('.mkv') // CHROME / OTHER BROWSERS CODE PATH
) // (your original "faster" approach)
// ----------------------
if (!file) { return new Promise((resolve, reject) => {
const error = new Error('No compatible video file found in torrent') this.log("Starting torrent download (Chrome path)");
this.log(error.message) this.client.add(magnetURI, (torrent) => {
if (status) status.textContent = 'Error: No video file found' this.handleChromeTorrent(torrent, videoElement, resolve, reject);
reject(error) });
return });
}
} catch (error) {
this.log("Failed to setup video streaming:", error);
throw error;
}
} }
videoElement.muted = true /**
videoElement.crossOrigin = 'anonymous' * The "faster" original approach for Chrome/other browsers.
*/
handleChromeTorrent(torrent, videoElement, resolve, reject) {
this.log("Torrent added (Chrome path): " + torrent.name);
videoElement.addEventListener('error', (e) => { const status = document.getElementById("status");
const error = e.target.error const progress = document.getElementById("progress");
this.log('Video error:', error) const peers = document.getElementById("peers");
if (error) { const speed = document.getElementById("speed");
this.log('Error code:', error.code) const downloaded = document.getElementById("downloaded");
this.log('Error message:', error.message)
}
if (status) status.textContent = 'Error playing video. Try disabling Brave Shields.'
})
videoElement.addEventListener('canplay', () => {
const playPromise = videoElement.play()
if (playPromise !== undefined) {
playPromise
.then(() => this.log('Autoplay started'))
.catch(err => {
this.log('Autoplay failed:', err)
if (status) status.textContent = 'Click to play video'
videoElement.addEventListener('click', () => {
videoElement.play()
.then(() => this.log('Play started by user'))
.catch(err => this.log('Play failed:', err))
}, { once: true })
})
}
})
videoElement.addEventListener('loadedmetadata', () => {
this.log('Video metadata loaded')
if (videoElement.duration === Infinity || isNaN(videoElement.duration)) {
this.log('Invalid duration, attempting to fix...')
videoElement.currentTime = 1e101
videoElement.currentTime = 0
}
})
try {
file.streamTo(videoElement)
this.log('Streaming started')
// Update stats every second
this.statsInterval = setInterval(() => {
if (!document.body.contains(videoElement)) {
clearInterval(this.statsInterval)
return
}
const percentage = torrent.progress * 100
if (progress) progress.style.width = `${percentage}%`
if (peers) peers.textContent = `Peers: ${torrent.numPeers}`
if (speed) speed.textContent = `${this.formatBytes(torrent.downloadSpeed)}/s`
if (downloaded) downloaded.textContent =
`${this.formatBytes(torrent.downloaded)} / ${this.formatBytes(torrent.length)}`
if (status) { if (status) {
status.textContent = torrent.progress === 1 status.textContent = `Loading ${torrent.name}...`;
}
// Find playable file (same as old code)
const file = torrent.files.find(
(f) =>
f.name.endsWith(".mp4") ||
f.name.endsWith(".webm") ||
f.name.endsWith(".mkv")
);
if (!file) {
const error = new Error("No compatible video file found in torrent");
this.log(error.message);
if (status) status.textContent = "Error: No video file found";
return reject(error);
}
// Mute for autoplay
videoElement.muted = true;
videoElement.crossOrigin = "anonymous";
// Error handling same as old code
videoElement.addEventListener("error", (e) => {
const errObj = e.target.error;
this.log("Video error:", errObj);
if (errObj) {
this.log("Error code:", errObj.code);
this.log("Error message:", errObj.message);
}
if (status) {
status.textContent =
"Error playing video. Try disabling Brave Shields.";
}
});
// Attempt autoplay
videoElement.addEventListener("canplay", () => {
const playPromise = videoElement.play();
if (playPromise !== undefined) {
playPromise
.then(() => this.log("Autoplay started (Chrome path)"))
.catch((err) => {
this.log("Autoplay failed:", err);
if (status) status.textContent = "Click to play video";
videoElement.addEventListener(
"click",
() => {
videoElement
.play()
.catch((err2) => this.log("Play failed:", err2));
},
{ once: true }
);
});
}
});
videoElement.addEventListener("loadedmetadata", () => {
this.log("Video metadata loaded (Chrome path)");
if (videoElement.duration === Infinity || isNaN(videoElement.duration)) {
this.log("Invalid duration, attempting to fix...");
videoElement.currentTime = 1e101;
videoElement.currentTime = 0;
}
});
// Now stream to the video element
try {
file.streamTo(videoElement); // no chunk constraints
this.log("Streaming started (Chrome path)");
// Update stats
this.statsInterval = setInterval(() => {
if (!document.body.contains(videoElement)) {
clearInterval(this.statsInterval);
return;
}
const percentage = torrent.progress * 100;
if (progress) progress.style.width = `${percentage}%`;
if (peers) peers.textContent = `Peers: ${torrent.numPeers}`;
if (speed) {
speed.textContent = `${this.formatBytes(torrent.downloadSpeed)}/s`;
}
if (downloaded) {
downloaded.textContent = `${this.formatBytes(
torrent.downloaded
)} / ${this.formatBytes(torrent.length)}`;
}
if (status) {
status.textContent =
torrent.progress === 1
? `${torrent.name}` ? `${torrent.name}`
: `Loading ${torrent.name}...` : `Loading ${torrent.name}...`;
} }
}, 1000) }, 1000);
this.currentTorrent = torrent this.currentTorrent = torrent;
resolve() resolve();
} catch (error) { } catch (err) {
this.log('Streaming error:', error) this.log("Streaming error (Chrome path):", err);
if (status) status.textContent = 'Error starting video stream' if (status) status.textContent = "Error starting video stream";
reject(error) reject(err);
} }
torrent.on('error', err => { // Torrent error event
this.log('Torrent error:', err) torrent.on("error", (err) => {
if (status) status.textContent = 'Error loading video' this.log("Torrent error (Chrome path):", err);
clearInterval(this.statsInterval) if (status) status.textContent = "Error loading video";
reject(err) clearInterval(this.statsInterval);
}) reject(err);
}) });
})
} catch (error) {
this.log('Failed to setup video streaming:', error)
throw error
}
} }
/**
* The new approach for Firefox: sequential, concurrency limit, smaller chunk size.
*/
handleFirefoxTorrent(torrent, videoElement, resolve, reject) {
this.log("Torrent added (Firefox path): " + torrent.name);
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) {
status.textContent = `Loading ${torrent.name}...`;
}
// Find playable file
const file = torrent.files.find(
(f) =>
f.name.endsWith(".mp4") ||
f.name.endsWith(".webm") ||
f.name.endsWith(".mkv")
);
if (!file) {
const error = new Error("No compatible video file found in torrent");
this.log(error.message);
if (status) status.textContent = "Error: No video file found";
return reject(error);
}
videoElement.muted = true;
videoElement.crossOrigin = "anonymous";
videoElement.addEventListener("error", (e) => {
const errObj = e.target.error;
this.log("Video error (Firefox path):", errObj);
if (errObj) {
this.log("Error code:", errObj.code);
this.log("Error message:", errObj.message);
}
if (status) {
status.textContent =
"Error playing video. Try disabling Brave Shields.";
}
});
videoElement.addEventListener("canplay", () => {
const playPromise = videoElement.play();
if (playPromise !== undefined) {
playPromise
.then(() => this.log("Autoplay started (Firefox path)"))
.catch((err) => {
this.log("Autoplay failed:", err);
if (status) status.textContent = "Click to play video";
videoElement.addEventListener(
"click",
() => {
videoElement
.play()
.catch((err2) => this.log("Play failed:", err2));
},
{ once: true }
);
});
}
});
videoElement.addEventListener("loadedmetadata", () => {
this.log("Video metadata loaded (Firefox path)");
if (videoElement.duration === Infinity || isNaN(videoElement.duration)) {
this.log("Invalid duration, attempting to fix...");
videoElement.currentTime = 1e101;
videoElement.currentTime = 0;
}
});
// We set a smaller chunk size for Firefox
try {
file.streamTo(videoElement, { highWaterMark: 32 * 1024 }); // 32 KB chunk
this.log("Streaming started (Firefox path)");
this.statsInterval = setInterval(() => {
if (!document.body.contains(videoElement)) {
clearInterval(this.statsInterval);
return;
}
const percentage = torrent.progress * 100;
if (progress) progress.style.width = `${percentage}%`;
if (peers) peers.textContent = `Peers: ${torrent.numPeers}`;
if (speed) {
speed.textContent = `${this.formatBytes(torrent.downloadSpeed)}/s`;
}
if (downloaded) {
downloaded.textContent = `${this.formatBytes(
torrent.downloaded
)} / ${this.formatBytes(torrent.length)}`;
}
if (status) {
status.textContent =
torrent.progress === 1
? `${torrent.name}`
: `Loading ${torrent.name}...`;
}
}, 1000);
this.currentTorrent = torrent;
resolve();
} catch (err) {
this.log("Streaming error (Firefox path):", err);
if (status) status.textContent = "Error starting video stream";
reject(err);
}
// Listen for torrent errors
torrent.on("error", (err) => {
this.log("Torrent error (Firefox path):", err);
if (status) status.textContent = "Error loading video";
clearInterval(this.statsInterval);
reject(err);
});
}
/**
* Clean up after playback or page unload.
*/
async cleanup() { async cleanup() {
try { try {
if (this.statsInterval) { if (this.statsInterval) {
clearInterval(this.statsInterval) clearInterval(this.statsInterval);
} }
if (this.currentTorrent) { if (this.currentTorrent) {
this.currentTorrent.destroy() this.currentTorrent.destroy();
} }
if (this.client) { if (this.client) {
await this.client.destroy() await this.client.destroy();
this.client = new WebTorrent() // Create a new client for future use // Recreate fresh client for next time
this.client = new WebTorrent();
} }
} catch (error) { } catch (error) {
this.log('Cleanup error:', error) this.log("Cleanup error:", error);
} }
} }
} }
export const torrentClient = new TorrentClient() export const torrentClient = new TorrentClient();