diff --git a/src/demo/config.json b/src/demo/config.json new file mode 100644 index 0000000..6c0e13c --- /dev/null +++ b/src/demo/config.json @@ -0,0 +1,3 @@ +{ + "torrentId": "magnet:?xt=urn:btih:a92964583d6bd03b5a420a474a96f2b47d19fd43&dn=SeedSigner_SingleSig_Guide.mp4&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=udp%3A%2F%2Fexplodie.org%3A6969&tr=udp%3A%2F%2Ftracker.empire-js.us%3A1337&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com" +} diff --git a/src/demo/css/style.css b/src/demo/css/style.css new file mode 100644 index 0000000..d77f40c --- /dev/null +++ b/src/demo/css/style.css @@ -0,0 +1,70 @@ +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif; + max-width: 800px; + margin: 20px auto; + padding: 0 20px; + background: #1a1a1a; + color: #ffffff; +} + +video { + width: 100%; + max-width: 800px; + margin: 20px 0; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0,0,0,0.3); + background: #000; +} + +.info-container { + background: #2a2a2a; + padding: 15px; + border-radius: 8px; + margin: 15px 0; + box-shadow: 0 2px 8px rgba(0,0,0,0.2); +} + +.progress-bar { + background: #3a3a3a; + height: 6px; + border-radius: 3px; + overflow: hidden; + margin: 10px 0; +} + +.progress-bar-fill { + background: #2196F3; + height: 100%; + width: 0; + transition: width 0.3s ease; + border-radius: 3px; +} + +.stats { + display: flex; + justify-content: space-between; + font-size: 14px; + color: #aaa; + margin-top: 8px; +} + +.status { + color: #2196F3; + font-size: 14px; + margin-bottom: 8px; +} + +h1 { + color: #fff; + font-size: 24px; + font-weight: 500; +} + +.peers { + display: flex; + gap: 15px; +} + +.speed { + color: #4CAF50; +} \ No newline at end of file diff --git a/src/demo/demo.html b/src/demo/demo.html new file mode 100644 index 0000000..c4abc51 --- /dev/null +++ b/src/demo/demo.html @@ -0,0 +1,32 @@ + + + + + + WebTorrent Video Demo + + + +

WebTorrent Video Demo

+ + + + + +
+
Initializing...
+
+
+
+
+
+ Peers: 0 + 0 KB/s +
+ 0 MB / 0 MB +
+
+ + + + \ No newline at end of file diff --git a/src/demo/js/main.js b/src/demo/js/main.js new file mode 100644 index 0000000..99db154 --- /dev/null +++ b/src/demo/js/main.js @@ -0,0 +1,296 @@ +import WebTorrent from 'https://esm.sh/webtorrent' + +const client = new WebTorrent() + +function log(msg) { + console.log(msg) +} + +// Check if running in Brave browser +async function isBrave() { + return (navigator.brave?.isBrave && await navigator.brave.isBrave()) || false +} + +// Longer timeout for Brave +const TIMEOUT_DURATION = 60000 // 60 seconds + +const torrentId = 'magnet:?xt=urn:btih:a92964583d6bd03b5a420a474a96f2b47d19fd43&dn=SeedSigner_SingleSig_Guide.mp4&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=udp%3A%2F%2Fexplodie.org%3A6969&tr=udp%3A%2F%2Ftracker.empire-js.us%3A1337&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com' + +async function waitForServiceWorkerActivation(registration) { + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error('Service worker activation timeout')) + }, TIMEOUT_DURATION) + + log('Waiting for service worker activation...') + + const checkActivation = () => { + if (registration.active) { + clearTimeout(timeout) + log('Service worker is active') + resolve(registration) + return true + } + return false + } + + // Check immediately + if (checkActivation()) return + + // Set up activation listener + registration.addEventListener('activate', () => { + checkActivation() + }) + + // Handle waiting state + if (registration.waiting) { + log('Service worker is waiting, sending skip waiting message') + registration.waiting.postMessage({ type: 'SKIP_WAITING' }) + } + + // Additional state change listener + registration.addEventListener('statechange', () => { + checkActivation() + }) + }) +} + +async function setupServiceWorker() { + try { + // Check for service worker support + if (!('serviceWorker' in navigator) || !navigator.serviceWorker) { + throw new Error('Service Worker not supported or disabled') + } + + // Brave-specific initialization + if (await isBrave()) { + log('Brave browser detected') + // Force clear any existing registrations in Brave + const registrations = await navigator.serviceWorker.getRegistrations() + for (const registration of registrations) { + await registration.unregister() + } + // Add delay for Brave's privacy checks + await new Promise(resolve => setTimeout(resolve, 1000)) + } + + // Get current path for service worker scope + const currentPath = window.location.pathname + const basePath = currentPath.substring(0, currentPath.lastIndexOf('/') + 1) + + // Register service worker with explicit scope + log('Registering service worker...') + const registration = await navigator.serviceWorker.register('./sw.min.js', { + scope: basePath, + updateViaCache: 'none' + }) + log('Service worker registered') + + // Wait for installation + if (registration.installing) { + log('Waiting for installation...') + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error('Installation timeout')) + }, TIMEOUT_DURATION) + + registration.installing.addEventListener('statechange', (e) => { + log('Service worker state:', e.target.state) + if (e.target.state === 'activated' || e.target.state === 'redundant') { + clearTimeout(timeout) + resolve() + } + }) + }) + } + + // Wait for activation + await waitForServiceWorkerActivation(registration) + log('Service worker activated') + + // Wait for ready state + const readyRegistration = await Promise.race([ + navigator.serviceWorker.ready, + new Promise((_, reject) => + setTimeout(() => reject(new Error('Service worker ready timeout')), TIMEOUT_DURATION) + ) + ]) + + if (!readyRegistration.active) { + throw new Error('Service worker not active after ready state') + } + + log('Service worker ready') + return registration + } catch (error) { + log('Service worker setup error:', error) + throw error + } +} + +function 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]}` +} + +function startTorrent() { + log('Starting torrent download') + client.add(torrentId, torrent => { + const status = document.querySelector('#status') + const progress = document.querySelector('#progress') + const video = document.querySelector('#video') + const peers = document.querySelector('#peers') + const speed = document.querySelector('#speed') + const downloaded = document.querySelector('#downloaded') + + log('Torrent added: ' + torrent.name) + status.textContent = `Loading ${torrent.name}...` + + const file = torrent.files.find(file => file.name.endsWith('.mp4')) + if (!file) { + log('No MP4 file found in torrent') + status.textContent = 'Error: No video file found' + return + } + + // Set up video element + video.muted = true + video.crossOrigin = 'anonymous' + + // Enhanced video error handling + video.addEventListener('error', (e) => { + const error = e.target.error + log('Video error:', error) + if (error) { + log('Error code:', error.code) + log('Error message:', error.message) + } + status.textContent = 'Error playing video. Try disabling Brave Shields for this site.' + }) + + video.addEventListener('canplay', () => { + const playPromise = video.play() + if (playPromise !== undefined) { + playPromise + .then(() => log('Autoplay started')) + .catch(err => { + log('Autoplay failed:', err) + status.textContent = 'Click to play video' + // Add click-to-play handler + video.addEventListener('click', () => { + video.play() + .then(() => log('Play started by user')) + .catch(err => log('Play failed:', err)) + }, { once: true }) + }) + } + }) + + // Handle metadata loading + video.addEventListener('loadedmetadata', () => { + log('Video metadata loaded') + if (video.duration === Infinity || isNaN(video.duration)) { + log('Invalid duration, attempting to fix...') + video.currentTime = 1e101 + video.currentTime = 0 + } + }) + + try { + file.streamTo(video) + log('Streaming started') + } catch (error) { + log('Streaming error:', error) + status.textContent = 'Error starting video stream' + } + + // Update stats every second + const statsInterval = setInterval(() => { + if (!document.body.contains(video)) { + clearInterval(statsInterval) + return + } + + const percentage = torrent.progress * 100 + progress.style.width = `${percentage}%` + peers.textContent = `Peers: ${torrent.numPeers}` + speed.textContent = `${formatBytes(torrent.downloadSpeed)}/s` + downloaded.textContent = `${formatBytes(torrent.downloaded)} / ${formatBytes(torrent.length)}` + + if (torrent.progress === 1) { + status.textContent = `${torrent.name}` + } else { + status.textContent = `Loading ${torrent.name}...` + } + }, 1000) + + torrent.on('error', err => { + log('Torrent error:', err) + status.textContent = 'Error loading video' + clearInterval(statsInterval) + }) + + // Cleanup handler + window.addEventListener('beforeunload', () => { + clearInterval(statsInterval) + client.destroy() + }) + }) +} + +async function init() { + try { + const isBraveBrowser = await isBrave() + + // Check for secure context + if (!window.isSecureContext) { + throw new Error('HTTPS or localhost required') + } + + // Brave-specific checks + if (isBraveBrowser) { + log('Checking Brave configuration...') + + // Check if service workers are enabled + if (!navigator.serviceWorker) { + throw new Error('Please enable Service Workers in Brave Shield settings') + } + + // Check for WebRTC + if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { + throw new Error('Please enable WebRTC in Brave Shield settings') + } + } + + log('Setting up service worker...') + const registration = await setupServiceWorker() + + if (!registration || !registration.active) { + throw new Error('Service worker setup failed') + } + + log('Service worker activated and ready') + + // Create WebTorrent server with activated service worker + client.createServer({ controller: registration }) + log('WebTorrent server created') + + // Start the torrent + startTorrent() + } catch (error) { + log('Initialization error:', error) + const status = document.querySelector('#status') + if (status) { + const errorMessage = await isBrave() + ? `${error.message} (Try disabling Brave Shields for this site)` + : error.message + status.textContent = 'Error initializing: ' + errorMessage + } + } +} + +// Start everything when page loads +window.addEventListener('load', init) \ No newline at end of file diff --git a/src/demo/sw.min.js b/src/demo/sw.min.js new file mode 100644 index 0000000..2a99835 --- /dev/null +++ b/src/demo/sw.min.js @@ -0,0 +1,132 @@ +(() => { + "use strict"; + + let cancelled = false; + + // Handle skip waiting message + self.addEventListener('message', event => { + if (event.data && event.data.type === 'SKIP_WAITING') { + self.skipWaiting() + } + }) + + // Immediately install and activate + self.addEventListener("install", () => { + self.skipWaiting() + }) + + // Claim clients on activation + self.addEventListener('activate', event => { + event.waitUntil( + Promise.all([ + clients.claim(), + self.skipWaiting(), + 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/")) { + return null; + } + + // Handle keepalive requests + if (t.includes(self.registration.scope + "webtorrent/keepalive/")) { + return new Response(); + } + + // Handle cancel requests + if (t.includes(self.registration.scope + "webtorrent/cancel/")) { + return new Response(new ReadableStream({ + cancel() { + cancelled = true; + } + })); + } + + // Handle streaming requests + return async function({ request: s }) { + const { url: t, method: n, headers: o, destination: a } = s; + + // Get all window clients + const l = 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]) + }; + s.postMessage({ + url: t, + method: n, + headers: Object.fromEntries(o.entries()), + scope: self.registration.scope, + destination: a, + type: "webtorrent" + }, [i]); + } + }); + + let c = null; + + const d = () => { + i.postMessage(false); + clearTimeout(c); + i.onmessage = null; + }; + + // Handle non-streaming response + if (r.body !== "STREAM") { + d(); + return new Response(r.body, r); + } + + // Handle streaming response + return new Response(new ReadableStream({ + pull: s => new Promise(t => { + i.onmessage = ({ data: e }) => { + if (e) { + s.enqueue(e); + } else { + d(); + s.close(); + } + t(); + }; + + if (!cancelled && a !== "document") { + clearTimeout(c); + c = setTimeout(() => { + d(); + t(); + }, 5000); + } + + i.postMessage(true); + }), + cancel() { + d(); + } + }), r); + }(s); + })(s); + + if (t) { + s.respondWith(t); + } + }); +})(); \ No newline at end of file