This commit is contained in:
2025-01-05 23:51:52 -05:00
parent 6a3143bda2
commit 0a78cb57a0
20 changed files with 20624 additions and 62 deletions

286
src/js/webtorrent.js Normal file
View File

@@ -0,0 +1,286 @@
// js/webtorrent.js
import WebTorrent from 'https://esm.sh/webtorrent'
export class TorrentClient {
constructor() {
this.client = new WebTorrent()
this.currentTorrent = null
this.TIMEOUT_DURATION = 60000 // 60 seconds
this.statsInterval = null
}
log(msg) {
console.log(msg)
}
async isBrave() {
return (navigator.brave?.isBrave && await navigator.brave.isBrave()) || false
}
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()
})
})
}
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 (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()
}
})
})
}
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) {
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]}`
}
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
}
}
async cleanup() {
try {
if (this.statsInterval) {
clearInterval(this.statsInterval)
}
if (this.currentTorrent) {
this.currentTorrent.destroy()
}
if (this.client) {
await this.client.destroy()
this.client = new WebTorrent() // Create a new client for future use
}
} catch (error) {
this.log('Cleanup error:', error)
}
}
}
export const torrentClient = new TorrentClient()