mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2025-09-08 06:58:43 +00:00
greatly improved playback speed on firefox based browsers
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1 +1,3 @@
|
|||||||
saved_config.yaml
|
saved_config.yaml
|
||||||
|
repo-context.txt
|
||||||
|
src/webtorrent-docs/
|
@@ -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>
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async waitForServiceWorkerActivation(registration) {
|
isFirefox() {
|
||||||
|
return /firefox/i.test(window.navigator.userAgent);
|
||||||
|
}
|
||||||
|
|
||||||
|
async waitForServiceWorkerActivation(registration) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
reject(new Error("Service worker activation timeout"));
|
||||||
|
}, this.TIMEOUT_DURATION);
|
||||||
|
|
||||||
|
this.log("Waiting for service worker activation...");
|
||||||
|
|
||||||
|
const checkActivation = () => {
|
||||||
|
if (registration.active) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
this.log("Service worker is active");
|
||||||
|
resolve(registration);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (checkActivation()) return;
|
||||||
|
|
||||||
|
registration.addEventListener("activate", () => {
|
||||||
|
checkActivation();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (registration.waiting) {
|
||||||
|
this.log("Service worker is waiting, sending skip waiting message");
|
||||||
|
registration.waiting.postMessage({ type: "SKIP_WAITING" });
|
||||||
|
}
|
||||||
|
|
||||||
|
registration.addEventListener("statechange", () => {
|
||||||
|
checkActivation();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers the service worker, waiting until it's fully active before proceeding.
|
||||||
|
*/
|
||||||
|
async setupServiceWorker() {
|
||||||
|
try {
|
||||||
|
const isBraveBrowser = await this.isBrave();
|
||||||
|
|
||||||
|
if (!window.isSecureContext) {
|
||||||
|
throw new Error("HTTPS or localhost required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!("serviceWorker" in navigator) || !navigator.serviceWorker) {
|
||||||
|
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) {
|
||||||
|
this.log("Checking Brave configuration...");
|
||||||
|
|
||||||
|
if (!navigator.serviceWorker) {
|
||||||
|
throw new Error(
|
||||||
|
"Please enable Service Workers in Brave Shield settings"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||||
|
throw new Error("Please enable WebRTC in Brave Shield settings");
|
||||||
|
}
|
||||||
|
|
||||||
|
const registrations = await navigator.serviceWorker.getRegistrations();
|
||||||
|
for (const registration of registrations) {
|
||||||
|
await registration.unregister();
|
||||||
|
}
|
||||||
|
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...");
|
||||||
|
const registration = await navigator.serviceWorker.register(
|
||||||
|
"./sw.min.js",
|
||||||
|
{
|
||||||
|
scope: basePath,
|
||||||
|
updateViaCache: "none",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this.log("Service worker registered");
|
||||||
|
|
||||||
|
if (registration.installing) {
|
||||||
|
this.log("Waiting for installation...");
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
reject(new Error("Installation timeout"));
|
||||||
|
}, this.TIMEOUT_DURATION);
|
||||||
|
|
||||||
|
registration.installing.addEventListener("statechange", (e) => {
|
||||||
|
this.log("Service worker state:", e.target.state);
|
||||||
|
if (
|
||||||
|
e.target.state === "activated" ||
|
||||||
|
e.target.state === "redundant"
|
||||||
|
) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for service worker to become active
|
||||||
|
await this.waitForServiceWorkerActivation(registration);
|
||||||
|
this.log("Service worker activated");
|
||||||
|
|
||||||
|
// Make sure it’s truly active
|
||||||
|
const readyRegistration = await Promise.race([
|
||||||
|
navigator.serviceWorker.ready,
|
||||||
|
new Promise((_, reject) =>
|
||||||
|
setTimeout(
|
||||||
|
() => reject(new Error("Service worker ready timeout")),
|
||||||
|
this.TIMEOUT_DURATION
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!readyRegistration.active) {
|
||||||
|
throw new Error("Service worker not active after ready state");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log("Service worker ready");
|
||||||
|
return registration;
|
||||||
|
} catch (error) {
|
||||||
|
this.log("Service worker setup error:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
formatBytes(bytes) {
|
||||||
|
if (bytes === 0) return "0 B";
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ["B", "KB", "MB", "GB"];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Streams the given magnet URI to the specified <video> element.
|
||||||
|
*/
|
||||||
|
async streamVideo(magnetURI, videoElement) {
|
||||||
|
try {
|
||||||
|
// 1) Setup service worker
|
||||||
|
const registration = await this.setupServiceWorker();
|
||||||
|
if (!registration || !registration.active) {
|
||||||
|
throw new Error("Service worker setup failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Create WebTorrent server AFTER service worker is ready
|
||||||
|
this.client.createServer({ controller: registration });
|
||||||
|
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) => {
|
||||||
const timeout = setTimeout(() => {
|
this.log("Starting torrent download (Firefox path)");
|
||||||
reject(new Error('Service worker activation timeout'))
|
this.client.add(
|
||||||
}, this.TIMEOUT_DURATION)
|
magnetURI,
|
||||||
|
{
|
||||||
this.log('Waiting for service worker activation...')
|
strategy: "sequential",
|
||||||
|
maxWebConns: 4, // reduce concurrency
|
||||||
const checkActivation = () => {
|
},
|
||||||
if (registration.active) {
|
(torrent) => {
|
||||||
clearTimeout(timeout)
|
this.handleFirefoxTorrent(torrent, videoElement, resolve, reject);
|
||||||
this.log('Service worker is active')
|
|
||||||
resolve(registration)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// ----------------------
|
||||||
|
// CHROME / OTHER BROWSERS CODE PATH
|
||||||
|
// (your original "faster" approach)
|
||||||
|
// ----------------------
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.log("Starting torrent download (Chrome path)");
|
||||||
|
this.client.add(magnetURI, (torrent) => {
|
||||||
|
this.handleChromeTorrent(torrent, videoElement, resolve, reject);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.log("Failed to setup video streaming:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (checkActivation()) return
|
/**
|
||||||
|
* The "faster" original approach for Chrome/other browsers.
|
||||||
|
*/
|
||||||
|
handleChromeTorrent(torrent, videoElement, resolve, reject) {
|
||||||
|
this.log("Torrent added (Chrome path): " + torrent.name);
|
||||||
|
|
||||||
registration.addEventListener('activate', () => {
|
const status = document.getElementById("status");
|
||||||
checkActivation()
|
const progress = document.getElementById("progress");
|
||||||
})
|
const peers = document.getElementById("peers");
|
||||||
|
const speed = document.getElementById("speed");
|
||||||
|
const downloaded = document.getElementById("downloaded");
|
||||||
|
|
||||||
if (registration.waiting) {
|
if (status) {
|
||||||
this.log('Service worker is waiting, sending skip waiting message')
|
status.textContent = `Loading ${torrent.name}...`;
|
||||||
registration.waiting.postMessage({ type: 'SKIP_WAITING' })
|
|
||||||
}
|
|
||||||
|
|
||||||
registration.addEventListener('statechange', () => {
|
|
||||||
checkActivation()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async setupServiceWorker() {
|
// Find playable file (same as old code)
|
||||||
try {
|
const file = torrent.files.find(
|
||||||
const isBraveBrowser = await this.isBrave()
|
(f) =>
|
||||||
|
f.name.endsWith(".mp4") ||
|
||||||
if (!window.isSecureContext) {
|
f.name.endsWith(".webm") ||
|
||||||
throw new Error('HTTPS or localhost required')
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
if (!('serviceWorker' in navigator) || !navigator.serviceWorker) {
|
// Mute for autoplay
|
||||||
throw new Error('Service Worker not supported or disabled')
|
videoElement.muted = true;
|
||||||
}
|
videoElement.crossOrigin = "anonymous";
|
||||||
|
|
||||||
if (isBraveBrowser) {
|
// Error handling same as old code
|
||||||
this.log('Checking Brave configuration...')
|
videoElement.addEventListener("error", (e) => {
|
||||||
|
const errObj = e.target.error;
|
||||||
if (!navigator.serviceWorker) {
|
this.log("Video error:", errObj);
|
||||||
throw new Error('Please enable Service Workers in Brave Shield settings')
|
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.";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
// Attempt autoplay
|
||||||
throw new Error('Please enable WebRTC in Brave Shield settings')
|
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 }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const registrations = await navigator.serviceWorker.getRegistrations()
|
videoElement.addEventListener("loadedmetadata", () => {
|
||||||
for (const registration of registrations) {
|
this.log("Video metadata loaded (Chrome path)");
|
||||||
await registration.unregister()
|
if (videoElement.duration === Infinity || isNaN(videoElement.duration)) {
|
||||||
}
|
this.log("Invalid duration, attempting to fix...");
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
videoElement.currentTime = 1e101;
|
||||||
}
|
videoElement.currentTime = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const currentPath = window.location.pathname
|
// Now stream to the video element
|
||||||
const basePath = currentPath.substring(0, currentPath.lastIndexOf('/') + 1)
|
try {
|
||||||
|
file.streamTo(videoElement); // no chunk constraints
|
||||||
this.log('Registering service worker...')
|
this.log("Streaming started (Chrome path)");
|
||||||
const registration = await navigator.serviceWorker.register('./sw.min.js', {
|
|
||||||
scope: basePath,
|
|
||||||
updateViaCache: 'none'
|
|
||||||
})
|
|
||||||
this.log('Service worker registered')
|
|
||||||
|
|
||||||
if (registration.installing) {
|
// Update stats
|
||||||
this.log('Waiting for installation...')
|
this.statsInterval = setInterval(() => {
|
||||||
await new Promise((resolve, reject) => {
|
if (!document.body.contains(videoElement)) {
|
||||||
const timeout = setTimeout(() => {
|
clearInterval(this.statsInterval);
|
||||||
reject(new Error('Installation timeout'))
|
return;
|
||||||
}, this.TIMEOUT_DURATION)
|
|
||||||
|
|
||||||
registration.installing.addEventListener('statechange', (e) => {
|
|
||||||
this.log('Service worker state:', e.target.state)
|
|
||||||
if (e.target.state === 'activated' || e.target.state === 'redundant') {
|
|
||||||
clearTimeout(timeout)
|
|
||||||
resolve()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.waitForServiceWorkerActivation(registration)
|
|
||||||
this.log('Service worker activated')
|
|
||||||
|
|
||||||
const readyRegistration = await Promise.race([
|
|
||||||
navigator.serviceWorker.ready,
|
|
||||||
new Promise((_, reject) =>
|
|
||||||
setTimeout(() => reject(new Error('Service worker ready timeout')), this.TIMEOUT_DURATION)
|
|
||||||
)
|
|
||||||
])
|
|
||||||
|
|
||||||
if (!readyRegistration.active) {
|
|
||||||
throw new Error('Service worker not active after ready state')
|
|
||||||
}
|
|
||||||
|
|
||||||
this.log('Service worker ready')
|
|
||||||
return registration
|
|
||||||
} catch (error) {
|
|
||||||
this.log('Service worker setup error:', error)
|
|
||||||
throw error
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
formatBytes(bytes) {
|
const percentage = torrent.progress * 100;
|
||||||
if (bytes === 0) return '0 B'
|
if (progress) progress.style.width = `${percentage}%`;
|
||||||
const k = 1024
|
if (peers) peers.textContent = `Peers: ${torrent.numPeers}`;
|
||||||
const sizes = ['B', 'KB', 'MB', 'GB']
|
if (speed) {
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
speed.textContent = `${this.formatBytes(torrent.downloadSpeed)}/s`;
|
||||||
return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`
|
|
||||||
}
|
|
||||||
|
|
||||||
async streamVideo(magnetURI, videoElement) {
|
|
||||||
try {
|
|
||||||
// Setup service worker first
|
|
||||||
const registration = await this.setupServiceWorker()
|
|
||||||
|
|
||||||
if (!registration || !registration.active) {
|
|
||||||
throw new Error('Service worker setup failed')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create WebTorrent server AFTER service worker is ready
|
|
||||||
this.client.createServer({ controller: registration })
|
|
||||||
this.log('WebTorrent server created')
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.log('Starting torrent download')
|
|
||||||
this.client.add(magnetURI, torrent => {
|
|
||||||
this.log('Torrent added: ' + 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}...`
|
|
||||||
|
|
||||||
const file = torrent.files.find(file =>
|
|
||||||
file.name.endsWith('.mp4') ||
|
|
||||||
file.name.endsWith('.webm') ||
|
|
||||||
file.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'
|
|
||||||
reject(error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
videoElement.muted = true
|
|
||||||
videoElement.crossOrigin = 'anonymous'
|
|
||||||
|
|
||||||
videoElement.addEventListener('error', (e) => {
|
|
||||||
const error = e.target.error
|
|
||||||
this.log('Video error:', error)
|
|
||||||
if (error) {
|
|
||||||
this.log('Error code:', error.code)
|
|
||||||
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) {
|
|
||||||
status.textContent = torrent.progress === 1
|
|
||||||
? `${torrent.name}`
|
|
||||||
: `Loading ${torrent.name}...`
|
|
||||||
}
|
|
||||||
}, 1000)
|
|
||||||
|
|
||||||
this.currentTorrent = torrent
|
|
||||||
resolve()
|
|
||||||
} catch (error) {
|
|
||||||
this.log('Streaming error:', error)
|
|
||||||
if (status) status.textContent = 'Error starting video stream'
|
|
||||||
reject(error)
|
|
||||||
}
|
|
||||||
|
|
||||||
torrent.on('error', err => {
|
|
||||||
this.log('Torrent error:', err)
|
|
||||||
if (status) status.textContent = 'Error loading video'
|
|
||||||
clearInterval(this.statsInterval)
|
|
||||||
reject(err)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
this.log('Failed to setup video streaming:', error)
|
|
||||||
throw error
|
|
||||||
}
|
}
|
||||||
|
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 (Chrome path):", err);
|
||||||
|
if (status) status.textContent = "Error starting video stream";
|
||||||
|
reject(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
async cleanup() {
|
// Torrent error event
|
||||||
try {
|
torrent.on("error", (err) => {
|
||||||
if (this.statsInterval) {
|
this.log("Torrent error (Chrome path):", err);
|
||||||
clearInterval(this.statsInterval)
|
if (status) status.textContent = "Error loading video";
|
||||||
}
|
clearInterval(this.statsInterval);
|
||||||
if (this.currentTorrent) {
|
reject(err);
|
||||||
this.currentTorrent.destroy()
|
});
|
||||||
}
|
}
|
||||||
if (this.client) {
|
|
||||||
await this.client.destroy()
|
/**
|
||||||
this.client = new WebTorrent() // Create a new client for future use
|
* The new approach for Firefox: sequential, concurrency limit, smaller chunk size.
|
||||||
}
|
*/
|
||||||
} catch (error) {
|
handleFirefoxTorrent(torrent, videoElement, resolve, reject) {
|
||||||
this.log('Cleanup error:', error)
|
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() {
|
||||||
|
try {
|
||||||
|
if (this.statsInterval) {
|
||||||
|
clearInterval(this.statsInterval);
|
||||||
|
}
|
||||||
|
if (this.currentTorrent) {
|
||||||
|
this.currentTorrent.destroy();
|
||||||
|
}
|
||||||
|
if (this.client) {
|
||||||
|
await this.client.destroy();
|
||||||
|
// Recreate fresh client for next time
|
||||||
|
this.client = new WebTorrent();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.log("Cleanup error:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const torrentClient = new TorrentClient()
|
export const torrentClient = new TorrentClient();
|
||||||
|
Reference in New Issue
Block a user