mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2025-09-09 15:38:44 +00:00
update
This commit is contained in:
@@ -1,241 +0,0 @@
|
|||||||
/* css/style.css */
|
|
||||||
|
|
||||||
/* Global Colors */
|
|
||||||
:root {
|
|
||||||
--color-bg: #1a1a2e;
|
|
||||||
--color-primary: #9b59b6; /* Purple */
|
|
||||||
--color-secondary: #e74c3c; /* Red */
|
|
||||||
--color-text: #ecf0f1; /* Light gray */
|
|
||||||
--color-muted: #95a5a6; /* Muted gray */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* General Reset */
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: 'Arial', sans-serif;
|
|
||||||
background-color: var(--color-bg);
|
|
||||||
color: var(--color-text);
|
|
||||||
line-height: 1.6;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Header */
|
|
||||||
header {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
header h1 {
|
|
||||||
font-size: 2.5rem;
|
|
||||||
color: var(--color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
header p {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
color: var(--color-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Buttons */
|
|
||||||
button {
|
|
||||||
background-color: var(--color-primary);
|
|
||||||
color: var(--color-text);
|
|
||||||
border: none;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
border-radius: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
background-color: var(--color-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
button:focus {
|
|
||||||
outline: 2px solid var(--color-primary);
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Forms */
|
|
||||||
form input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 0.5rem;
|
|
||||||
border: 1px solid var(--color-muted);
|
|
||||||
border-radius: 5px;
|
|
||||||
background-color: var(--color-bg);
|
|
||||||
color: var(--color-text);
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
form input:focus {
|
|
||||||
border-color: var(--color-primary);
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Video List */
|
|
||||||
#videoList {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
||||||
gap: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-card {
|
|
||||||
background-color: #24243e;
|
|
||||||
border-radius: 10px;
|
|
||||||
overflow: hidden;
|
|
||||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
|
|
||||||
transition: transform 0.3s;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-card:hover {
|
|
||||||
transform: translateY(-5px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-card img {
|
|
||||||
width: 100%;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-card .details {
|
|
||||||
padding: 1rem;
|
|
||||||
flex-grow: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-card h3 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
color: var(--color-primary);
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-card button {
|
|
||||||
width: 100%;
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Highlight dev mode videos */
|
|
||||||
.video-card.border-red-500 {
|
|
||||||
border: 2px solid red;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-card p.text-red-500 {
|
|
||||||
color: var(--color-secondary) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-card p.text-green-500 {
|
|
||||||
color: #2ecc71; /* Green */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Player Modal */
|
|
||||||
#playerModal {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background-color: rgba(0, 0, 0, 0.8);
|
|
||||||
display: none; /* Hidden by default */
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 1rem;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#playerModal.flex {
|
|
||||||
display: flex; /* Show modal as flex when active */
|
|
||||||
}
|
|
||||||
|
|
||||||
#playerModal .content {
|
|
||||||
background-color: var(--color-bg);
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 2rem;
|
|
||||||
max-width: 800px;
|
|
||||||
width: 100%;
|
|
||||||
color: var(--color-text);
|
|
||||||
position: relative; /* Ensure Close button is positioned correctly */
|
|
||||||
}
|
|
||||||
|
|
||||||
#closePlayer {
|
|
||||||
background-color: var(--color-secondary); /* Use secondary color for Close button */
|
|
||||||
color: var(--color-text);
|
|
||||||
border: none;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
border-radius: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
#closePlayer:hover {
|
|
||||||
background-color: var(--color-primary); /* Swap colors on hover for contrast */
|
|
||||||
}
|
|
||||||
|
|
||||||
#closePlayer:focus {
|
|
||||||
outline: 2px solid var(--color-primary);
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Notification Containers */
|
|
||||||
#errorContainer, #successContainer {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: 5px;
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#errorContainer {
|
|
||||||
background-color: #f8d7da;
|
|
||||||
color: #721c24;
|
|
||||||
}
|
|
||||||
|
|
||||||
#successContainer {
|
|
||||||
background-color: #d4edda;
|
|
||||||
color: #155724;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive Design */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
header h1 {
|
|
||||||
font-size: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
header p {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
padding: 0.4rem 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
form input {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
padding: 0.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-card h3 {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tailwind's hidden class */
|
|
||||||
.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
@@ -1,70 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
@@ -1,32 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>WebTorrent Video Demo</title>
|
|
||||||
<link rel="stylesheet" href="./css/style.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>WebTorrent Video Demo</h1>
|
|
||||||
|
|
||||||
<!-- Video player -->
|
|
||||||
<video id="video" controls></video>
|
|
||||||
|
|
||||||
<!-- Info Container -->
|
|
||||||
<div class="info-container">
|
|
||||||
<div class="status" id="status">Initializing...</div>
|
|
||||||
<div class="progress-bar">
|
|
||||||
<div class="progress-bar-fill" id="progress"></div>
|
|
||||||
</div>
|
|
||||||
<div class="stats">
|
|
||||||
<div class="peers">
|
|
||||||
<span id="peers">Peers: 0</span>
|
|
||||||
<span class="speed" id="speed">0 KB/s</span>
|
|
||||||
</div>
|
|
||||||
<span id="downloaded">0 MB / 0 MB</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script type="module" src="js/main.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@@ -1,296 +0,0 @@
|
|||||||
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)
|
|
132
src copy/demo/sw.min.js
vendored
132
src copy/demo/sw.min.js
vendored
@@ -1,132 +0,0 @@
|
|||||||
(() => {
|
|
||||||
"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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})();
|
|
@@ -1,103 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>NosTube MVP</title>
|
|
||||||
<!-- TailwindCSS CDN -->
|
|
||||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
|
||||||
<link href="css/style.css" rel="stylesheet">
|
|
||||||
</head>
|
|
||||||
<body class="bg-gray-100">
|
|
||||||
<div id="app" class="container mx-auto px-4 py-8">
|
|
||||||
<!-- Header -->
|
|
||||||
<header class="mb-8">
|
|
||||||
<h1 class="text-3xl font-bold text-gray-800">NosTube</h1>
|
|
||||||
<p class="text-gray-600">Decentralized video sharing</p>
|
|
||||||
<!-- Demo Link -->
|
|
||||||
<div class="mt-4">
|
|
||||||
<a href="./demo/demo.html" class="text-blue-500 hover:underline">
|
|
||||||
View Demo Page
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- Login Section -->
|
|
||||||
<div id="loginSection" class="mb-8 flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<button id="loginButton" class="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
|
|
||||||
Login with Nostr
|
|
||||||
</button>
|
|
||||||
<p id="userStatus" class="mt-4 text-gray-500 hidden">Logged in as: <span id="userPubKey"></span></p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<button id="logoutButton" class="bg-red-500 text-white px-4 py-2 rounded-md hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 hidden">
|
|
||||||
Logout
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Error Container -->
|
|
||||||
<div id="errorContainer" class="hidden bg-red-100 text-red-800 p-4 rounded-md mb-4">
|
|
||||||
<!-- Error messages will appear here -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Success Container -->
|
|
||||||
<div id="successContainer" class="hidden bg-green-100 text-green-800 p-4 rounded-md mb-4">
|
|
||||||
<!-- Success messages will appear here -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Video Submission Form -->
|
|
||||||
<div class="bg-white p-6 rounded-lg shadow-md mb-8 hidden" id="videoFormContainer">
|
|
||||||
<h2 class="text-xl font-semibold mb-4">Share a Video</h2>
|
|
||||||
<form id="submitForm" class="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label for="title" class="block text-sm font-medium text-gray-700">Title</label>
|
|
||||||
<input type="text" id="title" required
|
|
||||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="magnet" class="block text-sm font-medium text-gray-700">Magnet Link</label>
|
|
||||||
<input type="text" id="magnet" required
|
|
||||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="thumbnail" class="block text-sm font-medium text-gray-700">Thumbnail URL (optional)</label>
|
|
||||||
<input type="url" id="thumbnail"
|
|
||||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
|
||||||
</div>
|
|
||||||
<button type="submit"
|
|
||||||
class="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
|
|
||||||
Share Video
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Video List -->
|
|
||||||
<div id="videoList" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
||||||
<!-- Videos will be inserted here -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Video Player Modal -->
|
|
||||||
<div id="playerModal" class="hidden fixed inset-0 bg-black bg-opacity-50">
|
|
||||||
<div class="bg-white p-4 rounded-lg max-w-4xl w-full mx-4 relative">
|
|
||||||
<div id="player"></div>
|
|
||||||
<button id="closePlayer" class="mt-4 px-4 py-2 bg-gray-500 text-white rounded hover:bg-gray-600 absolute top-2 right-2">
|
|
||||||
Close
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Scripts -->
|
|
||||||
<!-- Load WebTorrent via CDN -->
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/webtorrent/1.9.7/webtorrent.min.js"></script>
|
|
||||||
<!-- Load Nostr library -->
|
|
||||||
<script src="js/libs/nostr.bundle.js"></script>
|
|
||||||
<!-- Load JavaScript Modules -->
|
|
||||||
<script type="module" src="js/config.js"></script>
|
|
||||||
<script type="module" src="js/webtorrent.js"></script>
|
|
||||||
<script type="module" src="js/nostr.js"></script>
|
|
||||||
<script type="module" src="js/app.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@@ -1,341 +0,0 @@
|
|||||||
// js/app.js
|
|
||||||
|
|
||||||
import { nostrClient } from './nostr.js';
|
|
||||||
import { torrentClient } from './webtorrent.js';
|
|
||||||
import { isDevMode } from './config.js';
|
|
||||||
|
|
||||||
class NosTubeApp {
|
|
||||||
constructor() {
|
|
||||||
this.loginButton = document.getElementById('loginButton');
|
|
||||||
this.logoutButton = document.getElementById('logoutButton'); // Added logout button
|
|
||||||
this.userStatus = document.getElementById('userStatus');
|
|
||||||
this.userPubKey = document.getElementById('userPubKey');
|
|
||||||
this.submitForm = document.getElementById('submitForm');
|
|
||||||
this.playerModal = document.getElementById('playerModal');
|
|
||||||
this.player = document.getElementById('player');
|
|
||||||
this.videoList = document.getElementById('videoList');
|
|
||||||
this.closePlayerBtn = document.getElementById('closePlayer');
|
|
||||||
this.errorContainer = document.getElementById('errorContainer');
|
|
||||||
this.successContainer = document.getElementById('successContainer');
|
|
||||||
this.videoFormContainer = document.getElementById('videoFormContainer'); // Added form container
|
|
||||||
|
|
||||||
this.pubkey = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the application by setting up the Nostr client and loading videos.
|
|
||||||
*/
|
|
||||||
async init() {
|
|
||||||
try {
|
|
||||||
// Ensure the modal is hidden by default
|
|
||||||
this.playerModal.classList.add('hidden');
|
|
||||||
|
|
||||||
// Hide the video submission form initially
|
|
||||||
this.videoFormContainer.classList.add('hidden');
|
|
||||||
|
|
||||||
// Initialize Nostr client
|
|
||||||
await nostrClient.init();
|
|
||||||
console.log('Nostr client initialized.');
|
|
||||||
|
|
||||||
// Check if user is already logged in (e.g., from localStorage)
|
|
||||||
const savedPubKey = localStorage.getItem('userPubKey');
|
|
||||||
if (savedPubKey) {
|
|
||||||
this.login(savedPubKey, false); // Do not prompt for login again
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup event listeners
|
|
||||||
this.setupEventListeners();
|
|
||||||
console.log('Event listeners set up.');
|
|
||||||
|
|
||||||
// Load videos
|
|
||||||
await this.loadVideos();
|
|
||||||
console.log('Videos loaded.');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to initialize app:', error);
|
|
||||||
this.showError('Failed to connect to Nostr relay. Please try again later.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets up event listeners for login, logout, form submission, and modal interactions.
|
|
||||||
*/
|
|
||||||
setupEventListeners() {
|
|
||||||
// Login Button
|
|
||||||
this.loginButton.addEventListener('click', async () => {
|
|
||||||
try {
|
|
||||||
const pubkey = await nostrClient.login();
|
|
||||||
this.login(pubkey, true);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Login failed:', error);
|
|
||||||
this.showError('Failed to login. Please try again.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Logout Button
|
|
||||||
this.logoutButton.addEventListener('click', () => {
|
|
||||||
this.logout();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Form submission
|
|
||||||
this.submitForm.addEventListener('submit', (e) => this.handleSubmit(e));
|
|
||||||
|
|
||||||
// Close player modal
|
|
||||||
if (this.closePlayerBtn) {
|
|
||||||
this.closePlayerBtn.addEventListener('click', () => {
|
|
||||||
console.log('Close button clicked. Hiding modal...');
|
|
||||||
this.hideModal();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.error('Close button not found!');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close modal when clicking outside the modal content
|
|
||||||
if (this.playerModal) {
|
|
||||||
this.playerModal.addEventListener('click', (e) => {
|
|
||||||
if (e.target === this.playerModal) {
|
|
||||||
console.log('Clicked outside modal content. Hiding modal...');
|
|
||||||
this.hideModal();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.error('playerModal not found!');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles user login by updating UI elements.
|
|
||||||
* @param {string} pubkey - The public key of the logged-in user.
|
|
||||||
* @param {boolean} saveToStorage - Whether to save the pubkey to localStorage.
|
|
||||||
*/
|
|
||||||
login(pubkey, saveToStorage = true) {
|
|
||||||
this.pubkey = pubkey;
|
|
||||||
this.loginButton.classList.add('hidden');
|
|
||||||
this.logoutButton.classList.remove('hidden');
|
|
||||||
this.userStatus.classList.remove('hidden');
|
|
||||||
this.userPubKey.textContent = pubkey;
|
|
||||||
this.videoFormContainer.classList.remove('hidden'); // Show form
|
|
||||||
console.log(`User logged in as: ${pubkey}`);
|
|
||||||
|
|
||||||
if (saveToStorage) {
|
|
||||||
localStorage.setItem('userPubKey', pubkey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Logs out the user by resetting UI elements and internal state.
|
|
||||||
*/
|
|
||||||
logout() {
|
|
||||||
nostrClient.logout();
|
|
||||||
this.pubkey = null;
|
|
||||||
this.loginButton.classList.remove('hidden');
|
|
||||||
this.logoutButton.classList.add('hidden');
|
|
||||||
this.userStatus.classList.add('hidden');
|
|
||||||
this.userPubKey.textContent = '';
|
|
||||||
this.videoFormContainer.classList.add('hidden'); // Hide form
|
|
||||||
localStorage.removeItem('userPubKey');
|
|
||||||
console.log('User logged out.');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hides the player modal, clears the player content, and stops streaming.
|
|
||||||
*/
|
|
||||||
async hideModal() {
|
|
||||||
if (this.playerModal) {
|
|
||||||
this.playerModal.classList.add('hidden');
|
|
||||||
this.playerModal.classList.remove('flex');
|
|
||||||
console.log('Modal hidden.');
|
|
||||||
} else {
|
|
||||||
console.error('playerModal is undefined.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.player) {
|
|
||||||
this.player.innerHTML = ''; // Clear video content when modal is closed
|
|
||||||
console.log('Player content cleared.');
|
|
||||||
} else {
|
|
||||||
console.error('player is undefined.');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await torrentClient.stopStreaming();
|
|
||||||
console.log('Streaming stopped.');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error stopping streaming:', error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the submission of a new video.
|
|
||||||
* @param {Event} e - The form submission event.
|
|
||||||
*/
|
|
||||||
async handleSubmit(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (!this.pubkey) {
|
|
||||||
this.showError('Please login to post a video.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const formData = {
|
|
||||||
title: document.getElementById('title').value.trim(),
|
|
||||||
magnet: document.getElementById('magnet').value.trim(),
|
|
||||||
thumbnail: document.getElementById('thumbnail').value.trim(),
|
|
||||||
mode: isDevMode ? 'dev' : 'live', // Add mode to the metadata
|
|
||||||
};
|
|
||||||
|
|
||||||
// Basic client-side validation
|
|
||||||
if (!formData.title || !formData.magnet) {
|
|
||||||
this.showError('Title and Magnet URI are required.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await nostrClient.publishVideo(formData, this.pubkey);
|
|
||||||
this.submitForm.reset();
|
|
||||||
await this.loadVideos(); // Refresh video list
|
|
||||||
this.showSuccess('Video shared successfully!');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to publish video:', error.message);
|
|
||||||
this.showError('Failed to share video. Please try again later.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads videos from the relays and renders them.
|
|
||||||
*/
|
|
||||||
async loadVideos() {
|
|
||||||
try {
|
|
||||||
const videos = await nostrClient.fetchVideos();
|
|
||||||
if (videos.length === 0) {
|
|
||||||
console.log('No valid videos found.');
|
|
||||||
}
|
|
||||||
this.renderVideoList(videos);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch videos:', error.message);
|
|
||||||
this.showError('An error occurred while loading videos. Please try again later.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders the list of videos in the UI.
|
|
||||||
* @param {Array} videos - An array of video objects to render.
|
|
||||||
*/
|
|
||||||
renderVideoList(videos) {
|
|
||||||
if (videos.length === 0) {
|
|
||||||
this.videoList.innerHTML = '<p class="text-center text-gray-500">No videos available yet. Be the first to upload one!</p>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort videos by creation date (newest first)
|
|
||||||
videos.sort((a, b) => b.created_at - a.created_at);
|
|
||||||
|
|
||||||
this.videoList.innerHTML = videos.map(video => `
|
|
||||||
<div class="video-card ${video.mode === 'dev' ? 'border border-red-500' : ''}">
|
|
||||||
<div class="aspect-w-16 aspect-h-9 bg-gray-100">
|
|
||||||
${video.thumbnail ?
|
|
||||||
`<img src="${this.escapeHTML(video.thumbnail)}" alt="${this.escapeHTML(video.title)}" class="object-cover w-full h-48">` :
|
|
||||||
'<div class="flex items-center justify-center h-48 bg-gray-200">No thumbnail</div>'
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="details p-4">
|
|
||||||
<h3 class="text-lg font-semibold mb-2">${this.escapeHTML(video.title)}</h3>
|
|
||||||
<p class="text-sm ${video.mode === 'dev' ? 'text-red-500' : 'text-green-500'}">
|
|
||||||
${video.mode.toUpperCase()}
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
onclick="app.playVideo('${encodeURIComponent(video.magnet)}')"
|
|
||||||
class="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
|
||||||
>
|
|
||||||
Play Video
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Plays the selected video using the torrent client.
|
|
||||||
* @param {string} magnetURI - The Magnet URI of the video to play.
|
|
||||||
*/
|
|
||||||
async playVideo(magnetURI) {
|
|
||||||
if (!magnetURI) {
|
|
||||||
this.showError('Invalid Magnet URI.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Opening video modal...');
|
|
||||||
this.playerModal.classList.remove('hidden');
|
|
||||||
this.playerModal.classList.add('flex');
|
|
||||||
console.log('Modal opened for video playback.');
|
|
||||||
|
|
||||||
try {
|
|
||||||
await torrentClient.streamVideo(decodeURIComponent(magnetURI), this.player);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to play video:', error.message);
|
|
||||||
this.showError('Failed to play video. Please try again.');
|
|
||||||
this.hideModal();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Displays an error message to the user.
|
|
||||||
* @param {string} message - The error message to display.
|
|
||||||
*/
|
|
||||||
showError(message) {
|
|
||||||
if (this.errorContainer) {
|
|
||||||
this.errorContainer.textContent = message;
|
|
||||||
this.errorContainer.classList.remove('hidden');
|
|
||||||
console.warn(`Error displayed to user: ${message}`);
|
|
||||||
|
|
||||||
// Hide the error message after 5 seconds
|
|
||||||
setTimeout(() => {
|
|
||||||
this.errorContainer.classList.add('hidden');
|
|
||||||
this.errorContainer.textContent = '';
|
|
||||||
}, 5000);
|
|
||||||
} else {
|
|
||||||
console.warn('Error container not found. Falling back to alert.');
|
|
||||||
alert(message); // Fallback for missing error container
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Displays a success message to the user.
|
|
||||||
* @param {string} message - The success message to display.
|
|
||||||
*/
|
|
||||||
showSuccess(message) {
|
|
||||||
if (this.successContainer) {
|
|
||||||
this.successContainer.textContent = message;
|
|
||||||
this.successContainer.classList.remove('hidden');
|
|
||||||
console.log(`Success message displayed: ${message}`);
|
|
||||||
|
|
||||||
// Hide the success message after 5 seconds
|
|
||||||
setTimeout(() => {
|
|
||||||
this.successContainer.classList.add('hidden');
|
|
||||||
this.successContainer.textContent = '';
|
|
||||||
}, 5000);
|
|
||||||
} else {
|
|
||||||
console.log('Success container not found. Falling back to alert.');
|
|
||||||
alert(message); // Fallback for missing success container
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Escapes HTML characters to prevent XSS attacks.
|
|
||||||
* @param {string} unsafe - The string to escape.
|
|
||||||
* @returns {string} The escaped string.
|
|
||||||
*/
|
|
||||||
escapeHTML(unsafe) {
|
|
||||||
return unsafe
|
|
||||||
.replace(/&/g, "&")
|
|
||||||
.replace(/</g, "<")
|
|
||||||
.replace(/>/g, ">")
|
|
||||||
.replace(/"/g, """)
|
|
||||||
.replace(/'/g, "'");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize app
|
|
||||||
const app = new NosTubeApp();
|
|
||||||
app.init();
|
|
||||||
|
|
||||||
// Make playVideo accessible globally for the onclick handlers
|
|
||||||
window.app = app;
|
|
@@ -1,3 +0,0 @@
|
|||||||
// js/config.js
|
|
||||||
|
|
||||||
export const isDevMode = true; // Set to false for production
|
|
File diff suppressed because it is too large
Load Diff
@@ -1,227 +0,0 @@
|
|||||||
// js/nostr.js
|
|
||||||
|
|
||||||
import { isDevMode } from './config.js';
|
|
||||||
|
|
||||||
const RELAY_URLS = [
|
|
||||||
'wss://relay.damus.io',
|
|
||||||
'wss://nos.lol',
|
|
||||||
'wss://relay.snort.social',
|
|
||||||
'wss://nostr.wine'
|
|
||||||
];
|
|
||||||
|
|
||||||
class NostrClient {
|
|
||||||
constructor() {
|
|
||||||
this.pool = new window.NostrTools.SimplePool(); // Access via window
|
|
||||||
this.pubkey = null;
|
|
||||||
this.relays = RELAY_URLS;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the Nostr client by connecting to relays.
|
|
||||||
*/
|
|
||||||
async init() {
|
|
||||||
try {
|
|
||||||
console.log('Connecting to relays...');
|
|
||||||
|
|
||||||
// Test relay connections
|
|
||||||
const testFilter = { kinds: [0], limit: 1 }; // Dummy filter for testing
|
|
||||||
const connections = this.relays.map(async url => {
|
|
||||||
try {
|
|
||||||
const sub = this.pool.sub([url], [testFilter]);
|
|
||||||
sub.on('event', event => console.log(`Test event from ${url}:`, event));
|
|
||||||
sub.on('eose', () => {
|
|
||||||
console.log(`Relay ${url} connected successfully.`);
|
|
||||||
sub.unsub();
|
|
||||||
});
|
|
||||||
return { url, success: true };
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`Failed to connect to relay: ${url}`, err.message);
|
|
||||||
return { url, success: false };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const results = await Promise.all(connections);
|
|
||||||
const successfulRelays = results.filter(r => r.success).map(r => r.url);
|
|
||||||
if (successfulRelays.length === 0) {
|
|
||||||
throw new Error('No relays could be connected.');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Connected to ${successfulRelays.length} relay(s):`, successfulRelays);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to initialize Nostr client:', err.message);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Logs in the user using a Nostr extension or by entering an NSEC key.
|
|
||||||
* @returns {Promise<string>} The public key of the logged-in user.
|
|
||||||
*/
|
|
||||||
async login() {
|
|
||||||
if (window.nostr) {
|
|
||||||
try {
|
|
||||||
const pubkey = await window.nostr.getPublicKey();
|
|
||||||
this.pubkey = pubkey;
|
|
||||||
console.log('Logged in with extension. Public key:', this.pubkey);
|
|
||||||
return this.pubkey;
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Failed to get public key from Nostr extension:', e.message);
|
|
||||||
throw new Error('Failed to get public key from Nostr extension.');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const nsec = prompt('Enter your NSEC key:');
|
|
||||||
if (nsec) {
|
|
||||||
try {
|
|
||||||
this.pubkey = this.decodeNsec(nsec);
|
|
||||||
console.log('Logged in with NSEC. Public key:', this.pubkey);
|
|
||||||
return this.pubkey;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Invalid NSEC key:', error.message);
|
|
||||||
throw new Error('Invalid NSEC key.');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new Error('Login cancelled or NSEC key not provided.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Logs out the user by clearing the public key.
|
|
||||||
*/
|
|
||||||
logout() {
|
|
||||||
this.pubkey = null;
|
|
||||||
console.log('User logged out.');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes an NSEC key to retrieve the public key.
|
|
||||||
* @param {string} nsec - The NSEC key.
|
|
||||||
* @returns {string} The corresponding public key.
|
|
||||||
*/
|
|
||||||
decodeNsec(nsec) {
|
|
||||||
try {
|
|
||||||
const { data } = window.NostrTools.nip19.decode(nsec); // Access via window
|
|
||||||
return data;
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error('Invalid NSEC key.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Publishes a new video event to all relays.
|
|
||||||
* @param {Object} videoData - The video data to publish.
|
|
||||||
* @param {string} pubkey - The public key of the user publishing the video.
|
|
||||||
* @returns {Promise<Object>} The signed event.
|
|
||||||
*/
|
|
||||||
async publishVideo(videoData, pubkey) {
|
|
||||||
if (!pubkey) {
|
|
||||||
throw new Error('User is not logged in.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const event = {
|
|
||||||
kind: 30078,
|
|
||||||
pubkey,
|
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
|
||||||
tags: [['t', 'video']],
|
|
||||||
content: JSON.stringify(videoData)
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const signedEvent = await window.nostr.signEvent(event);
|
|
||||||
await Promise.all(this.relays.map(async url => {
|
|
||||||
try {
|
|
||||||
await this.pool.publish([url], signedEvent);
|
|
||||||
console.log(`Event published to ${url}`);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`Failed to publish to ${url}:`, err.message);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
return signedEvent;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to sign event:', error.message);
|
|
||||||
throw new Error('Failed to sign event.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches videos from all configured relays, ensuring that only valid video events are included.
|
|
||||||
* Filters videos based on the current mode (dev or live).
|
|
||||||
* @returns {Promise<Array>} An array of valid video objects.
|
|
||||||
*/
|
|
||||||
async fetchVideos() {
|
|
||||||
const filter = {
|
|
||||||
kinds: [30078], // Video kind
|
|
||||||
limit: 50 // Fetch up to 50 videos
|
|
||||||
};
|
|
||||||
const videos = new Map(); // Use a Map to ensure unique videos by event ID
|
|
||||||
|
|
||||||
// Initialize summary counters
|
|
||||||
const invalidEventSummary = {
|
|
||||||
malformedJson: 0,
|
|
||||||
invalidFormat: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
// Fetch videos from all relays
|
|
||||||
await Promise.all(
|
|
||||||
this.relays.map(async url => {
|
|
||||||
try {
|
|
||||||
const events = await this.pool.list([url], [filter]);
|
|
||||||
events.forEach(event => {
|
|
||||||
try {
|
|
||||||
const content = JSON.parse(event.content);
|
|
||||||
// Filter by mode
|
|
||||||
if (content.mode === (isDevMode ? 'dev' : 'live') && this.isValidVideo(content)) {
|
|
||||||
if (!videos.has(event.id)) { // Ensure uniqueness
|
|
||||||
videos.set(event.id, {
|
|
||||||
id: event.id,
|
|
||||||
title: content.title,
|
|
||||||
magnet: content.magnet,
|
|
||||||
thumbnail: content.thumbnail || '',
|
|
||||||
mode: content.mode,
|
|
||||||
pubkey: event.pubkey,
|
|
||||||
created_at: event.created_at,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (jsonError) {
|
|
||||||
invalidEventSummary.malformedJson++;
|
|
||||||
console.error(
|
|
||||||
`Failed to parse video content from ${url}: ${jsonError.message} | Event ID: ${event.id}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (relayError) {
|
|
||||||
console.error(`Failed to fetch videos from relay ${url}: ${relayError.message}`);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Log a summary of issues
|
|
||||||
if (invalidEventSummary.malformedJson > 0) {
|
|
||||||
console.warn(`Skipped ${invalidEventSummary.malformedJson} event(s) due to malformed JSON.`);
|
|
||||||
}
|
|
||||||
if (invalidEventSummary.invalidFormat > 0) {
|
|
||||||
console.warn(`Skipped ${invalidEventSummary.invalidFormat} event(s) due to invalid format.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Array.from(videos.values()); // Return unique videos as an array
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates the structure of a video content object.
|
|
||||||
* @param {Object} content - The content object to validate.
|
|
||||||
* @returns {boolean} True if valid, false otherwise.
|
|
||||||
*/
|
|
||||||
isValidVideo(content) {
|
|
||||||
return (
|
|
||||||
typeof content === 'object' &&
|
|
||||||
typeof content.title === 'string' &&
|
|
||||||
typeof content.magnet === 'string' &&
|
|
||||||
typeof content.mode === 'string' &&
|
|
||||||
(typeof content.thumbnail === 'string' || typeof content.thumbnail === 'undefined')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export the client
|
|
||||||
export const nostrClient = new NostrClient();
|
|
@@ -1,122 +0,0 @@
|
|||||||
// js/webtorrent.js
|
|
||||||
|
|
||||||
export class TorrentClient {
|
|
||||||
constructor() {
|
|
||||||
this.client = new WebTorrent();
|
|
||||||
this.currentTorrent = null;
|
|
||||||
|
|
||||||
// Handle client-level errors
|
|
||||||
this.client.on('error', (err) => {
|
|
||||||
console.error('WebTorrent client error:', err.message);
|
|
||||||
// Optionally, emit events or handle errors globally
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Streams a video from a given Magnet URI into a specified HTML element.
|
|
||||||
* @param {string} magnetURI - The Magnet URI of the torrent.
|
|
||||||
* @param {HTMLElement} playerElement - The HTML element where the video will be rendered.
|
|
||||||
* @returns {Promise<void>} Resolves when streaming starts successfully.
|
|
||||||
*/
|
|
||||||
streamVideo(magnetURI, playerElement) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (!magnetURI) {
|
|
||||||
reject(new Error('Magnet URI is required.'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Adding torrent: ${magnetURI}`);
|
|
||||||
|
|
||||||
// If there's an existing torrent, remove it first
|
|
||||||
if (this.currentTorrent) {
|
|
||||||
console.log('Removing existing torrent before adding a new one.');
|
|
||||||
this.client.remove(this.currentTorrent, (err) => {
|
|
||||||
if (err) {
|
|
||||||
console.error('Error removing existing torrent:', err.message);
|
|
||||||
// Proceed to add the new torrent even if removal fails
|
|
||||||
}
|
|
||||||
this._addTorrent(magnetURI, playerElement, resolve, reject);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this._addTorrent(magnetURI, playerElement, resolve, reject);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a torrent and streams the video.
|
|
||||||
* @private
|
|
||||||
* @param {string} magnetURI - The Magnet URI of the torrent.
|
|
||||||
* @param {HTMLElement} playerElement - The HTML element where the video will be rendered.
|
|
||||||
* @param {Function} resolve - The resolve function of the Promise.
|
|
||||||
* @param {Function} reject - The reject function of the Promise.
|
|
||||||
*/
|
|
||||||
_addTorrent(magnetURI, playerElement, resolve, reject) {
|
|
||||||
this.client.add(magnetURI, (torrent) => {
|
|
||||||
this.currentTorrent = torrent;
|
|
||||||
console.log('Torrent metadata received:', torrent.infoHash);
|
|
||||||
|
|
||||||
// Find the first compatible video file in the torrent
|
|
||||||
const file = torrent.files.find(file => {
|
|
||||||
return file.name.endsWith('.mp4') ||
|
|
||||||
file.name.endsWith('.webm') ||
|
|
||||||
file.name.endsWith('.mkv');
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!file) {
|
|
||||||
console.error('No compatible video file found in the torrent.');
|
|
||||||
reject(new Error('No compatible video file found in the torrent.'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Streaming file:', file.name);
|
|
||||||
|
|
||||||
// Use renderTo for better compatibility and simplicity
|
|
||||||
file.renderTo(playerElement, { autoplay: true, controls: true }, (err, elem) => {
|
|
||||||
if (err) {
|
|
||||||
console.error('Error rendering video:', err);
|
|
||||||
reject(err);
|
|
||||||
} else {
|
|
||||||
console.log('Video rendered successfully.');
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle torrent-specific errors
|
|
||||||
this.client.on('torrent', (torrent) => {
|
|
||||||
torrent.on('error', (err) => {
|
|
||||||
console.error(`Torrent error (${torrent.infoHash}):`, err.message);
|
|
||||||
reject(err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stops streaming the current torrent and cleans up resources.
|
|
||||||
* @returns {Promise<void>} Resolves when the torrent is successfully removed.
|
|
||||||
*/
|
|
||||||
stopStreaming() {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (this.currentTorrent) {
|
|
||||||
console.log('Removing current torrent:', this.currentTorrent.infoHash);
|
|
||||||
this.client.remove(this.currentTorrent, (err) => {
|
|
||||||
if (err) {
|
|
||||||
console.error('Error removing torrent:', err.message);
|
|
||||||
reject(err);
|
|
||||||
} else {
|
|
||||||
console.log('Torrent removed successfully.');
|
|
||||||
this.currentTorrent = null;
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.warn('No active torrent to stop.');
|
|
||||||
resolve(); // Nothing to do
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export an instance of TorrentClient
|
|
||||||
export const torrentClient = new TorrentClient();
|
|
132
src copy/sw.min.js
vendored
132
src copy/sw.min.js
vendored
@@ -1,132 +0,0 @@
|
|||||||
(() => {
|
|
||||||
"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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})();
|
|
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
@@ -1,70 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
@@ -1,32 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>WebTorrent Video Demo</title>
|
|
||||||
<link rel="stylesheet" href="./css/style.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>WebTorrent Video Demo</h1>
|
|
||||||
|
|
||||||
<!-- Video player -->
|
|
||||||
<video id="video" controls></video>
|
|
||||||
|
|
||||||
<!-- Info Container -->
|
|
||||||
<div class="info-container">
|
|
||||||
<div class="status" id="status">Initializing...</div>
|
|
||||||
<div class="progress-bar">
|
|
||||||
<div class="progress-bar-fill" id="progress"></div>
|
|
||||||
</div>
|
|
||||||
<div class="stats">
|
|
||||||
<div class="peers">
|
|
||||||
<span id="peers">Peers: 0</span>
|
|
||||||
<span class="speed" id="speed">0 KB/s</span>
|
|
||||||
</div>
|
|
||||||
<span id="downloaded">0 MB / 0 MB</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script type="module" src="js/main.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@@ -1,296 +0,0 @@
|
|||||||
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)
|
|
132
src/demo/sw.min.js
vendored
132
src/demo/sw.min.js
vendored
@@ -1,132 +0,0 @@
|
|||||||
(() => {
|
|
||||||
"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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})();
|
|
Reference in New Issue
Block a user