// HLS Video Player with statistics and sharing class VideoPlayer { constructor() { this.hls = null; this.video = null; this.videoHash = null; this.videoName = null; this.webTorrentClient = null; this.currentTorrent = null; this.isP2PEnabled = false; this.stats = { startTime: Date.now(), bytesLoaded: 0, droppedFrames: 0, lastBytesLoaded: 0, lastTime: Date.now() }; this.initializeFromURL(); this.initializePlayer(); this.initializeTheme(); this.setupEventListeners(); // Update stats every second setInterval(() => this.updatePlaybackStats(), 1000); // Initialize WebTorrent client if (typeof WebTorrent !== 'undefined') { this.webTorrentClient = new WebTorrent(); console.log('WebTorrent client initialized'); } else { console.warn('WebTorrent not available'); } } initializeFromURL() { const urlParams = new URLSearchParams(window.location.search); this.videoHash = urlParams.get('hash'); this.videoName = urlParams.get('name') || 'Unknown Video'; if (!this.videoHash) { this.showError('No video hash provided in URL'); return; } document.getElementById('video-title').textContent = this.videoName; // Initialize hash display immediately if (this.videoHash) { document.getElementById('video-hash').textContent = this.videoHash.substring(0, 8) + '...'; document.getElementById('video-hash').title = this.videoHash; } this.setupShareLinks(); } initializePlayer() { this.video = document.getElementById('video-player'); if (!this.videoHash) return; // Check if this is an MKV file - don't attempt browser playback const isMKV = this.videoName && this.videoName.toLowerCase().endsWith('.mkv'); if (isMKV) { console.log('MKV file detected - showing download options instead of browser playback'); this.showMKVDownloadInterface(); return; } // Use direct streaming for non-MKV files console.log('Initializing direct video streaming'); this.initializeDirectStreaming(); } initializeDirectStreaming() { const directUrl = `/api/stream/${this.videoHash}`; this.video.src = directUrl; // Add event listeners for direct streaming this.video.addEventListener('loadedmetadata', () => { console.log('Video metadata loaded'); this.updateVideoInfo(); }); this.video.addEventListener('canplay', () => { console.log('Video can start playing'); this.updateVideoInfo(); }); this.video.addEventListener('error', (e) => { console.error('Video error:', e, this.video.error); this.handleVideoError(); }); this.video.addEventListener('progress', () => { this.updateBufferInfo(); this.updateNetworkStats(); }); // Load the video this.video.load(); } handleVideoError() { const error = this.video.error; let errorMessage = 'Video playback failed'; let showExternalPlayerOption = false; // Check if this is an MKV file const isMKV = this.videoName && this.videoName.toLowerCase().endsWith('.mkv'); if (error) { switch (error.code) { case error.MEDIA_ERR_ABORTED: errorMessage = 'Video playback was aborted'; break; case error.MEDIA_ERR_NETWORK: errorMessage = 'Network error occurred while loading video'; break; case error.MEDIA_ERR_DECODE: if (isMKV) { errorMessage = 'MKV files are not supported in web browsers'; showExternalPlayerOption = true; } else { errorMessage = 'Video format is not supported or corrupted'; } break; case error.MEDIA_ERR_SRC_NOT_SUPPORTED: if (isMKV) { errorMessage = 'MKV files require external video players'; showExternalPlayerOption = true; } else { errorMessage = 'Video source is not supported'; } break; default: errorMessage = `Unknown video error (code: ${error.code})`; if (isMKV) { showExternalPlayerOption = true; } } } this.showError(errorMessage, showExternalPlayerOption); } showMKVDownloadInterface() { const videoContainer = document.querySelector('.video-container'); videoContainer.innerHTML = `
🎬

MKV File Detected

Browser Compatibility Notice:
MKV files cannot be played directly in web browsers due to codec limitations. Both Firefox and Chrome have limited or no support for the Matroska container format.

🔧 Technical Details:

  • Firefox: No native MKV support
  • Chrome: Partial support, often audio issues
  • Codec: Your file likely uses DDP5.1 audio

📥 Available Options:

💡 Recommended: Use VLC Media Player, MPV, or similar desktop players for best MKV playback experience.
`; // Hide video controls and quality selector since we're not using video element this.setupQualitySelector(); } updateBufferInfo() { if (this.video.buffered.length > 0) { const bufferedEnd = this.video.buffered.end(this.video.buffered.length - 1); const bufferHealth = Math.max(0, bufferedEnd - this.video.currentTime); document.getElementById('buffer-health').textContent = `${bufferHealth.toFixed(1)}s`; } } initializeTheme() { const savedTheme = localStorage.getItem('theme') || 'light'; document.documentElement.setAttribute('data-theme', savedTheme); } setupEventListeners() { // Video events this.video.addEventListener('loadstart', () => { console.log('Video load started'); }); this.video.addEventListener('loadedmetadata', () => { this.updateVideoInfo(); }); this.video.addEventListener('play', () => { console.log('Video playback started'); }); this.video.addEventListener('error', (e) => { console.error('Video error:', e); this.showError('Video playback error'); }); // Quality selector const qualitySelect = document.getElementById('quality-select'); qualitySelect.addEventListener('change', (e) => { this.changeQuality(e.target.value); }); } setupQualitySelector() { // Hide quality selector for direct streaming as we serve native quality document.getElementById('quality-selector').classList.add('hidden'); } changeQuality(qualityIndex) { if (!this.hls) return; if (qualityIndex === 'auto') { this.hls.currentLevel = -1; // Auto quality } else { this.hls.currentLevel = parseInt(qualityIndex); } this.updateCurrentQuality(); } updateVideoInfo() { // Update video metadata display - show first 8 chars + ellipsis if (this.videoHash) { document.getElementById('video-hash').textContent = this.videoHash.substring(0, 8) + '...'; document.getElementById('video-hash').title = this.videoHash; // Full hash on hover } if (this.video.duration && isFinite(this.video.duration)) { document.getElementById('video-duration').textContent = this.formatTime(this.video.duration); } // Get file size from metadata this.fetchVideoMetadata(); } async fetchVideoMetadata() { try { // Try to get metadata from the gateway API const response = await fetch(`/api/info/${this.videoHash}`); if (response.ok) { const data = await response.json(); console.log('Video metadata:', data); if (data.size) { this.videoSize = data.size; document.getElementById('video-size').textContent = this.formatBytes(data.size); } // Update video title with actual filename if available if (data.name && data.name !== 'Unknown Video') { document.getElementById('video-title').textContent = data.name; this.videoName = data.name; } // Update duration from metadata if video element doesn't have it if (data.duration && (!this.video.duration || isNaN(this.video.duration))) { document.getElementById('video-duration').textContent = this.formatTime(data.duration); } } } catch (error) { console.log('Could not fetch video metadata:', error); } } updatePlaybackStats() { if (!this.video) return; // Update current quality this.updateCurrentQuality(); // Update buffer health if (this.video.buffered.length > 0) { const bufferedEnd = this.video.buffered.end(this.video.buffered.length - 1); const bufferHealth = Math.max(0, bufferedEnd - this.video.currentTime); document.getElementById('buffer-health').textContent = `${bufferHealth.toFixed(1)}s`; } // Update dropped frames (if available) if (this.video.getVideoPlaybackQuality) { const quality = this.video.getVideoPlaybackQuality(); document.getElementById('dropped-frames').textContent = quality.droppedVideoFrames || 0; } } updateCurrentQuality() { // For direct streaming, show the native video quality if available if (this.video.videoWidth && this.video.videoHeight) { document.getElementById('current-quality').textContent = `${this.video.videoHeight}p (Native)`; } else { document.getElementById('current-quality').textContent = 'Loading...'; } } updateNetworkStats() { if (!this.video.buffered.length) return; const currentTime = Date.now(); const elapsed = (currentTime - this.stats.lastTime) / 1000; if (elapsed > 1) { // Update every second // Estimate bytes loaded from buffer const bufferedBytes = this.estimateBufferedBytes(); const bytesDiff = bufferedBytes - this.stats.lastBytesLoaded; if (bytesDiff > 0 && elapsed > 0) { const speed = bytesDiff / elapsed; document.getElementById('network-speed').textContent = `${this.formatBytes(speed)}/s`; } this.stats.lastBytesLoaded = bufferedBytes; this.stats.lastTime = currentTime; } } estimateBufferedBytes() { if (!this.video.buffered.length || !this.video.duration) return 0; let totalBuffered = 0; for (let i = 0; i < this.video.buffered.length; i++) { totalBuffered += this.video.buffered.end(i) - this.video.buffered.start(i); } // Estimate bytes based on duration ratio (rough approximation) const bufferedRatio = totalBuffered / this.video.duration; return bufferedRatio * (this.videoSize || 0); } setupShareLinks() { if (!this.videoHash) return; const baseUrl = window.location.origin; document.getElementById('direct-link').value = `${baseUrl}/player.html?hash=${this.videoHash}&name=${encodeURIComponent(this.videoName)}`; document.getElementById('hls-link').value = `${baseUrl}/api/stream/${this.videoHash}/playlist.m3u8`; document.getElementById('torrent-link').value = `${baseUrl}/api/torrent/${this.videoHash}`; // Magnet link would need to be fetched from the server this.fetchMagnetLink(); } async fetchMagnetLink() { try { const response = await fetch(`/api/info/${this.videoHash}`); if (response.ok) { const data = await response.json(); if (data.magnet_link) { document.getElementById('magnet-link').value = data.magnet_link; } console.log('Magnet link data:', data); } } catch (error) { console.log('Could not fetch magnet link:', error); } } handleFatalError(data) { let errorMessage = 'Fatal playback error'; switch (data.type) { case Hls.ErrorTypes.NETWORK_ERROR: errorMessage = 'Network error - check your connection'; break; case Hls.ErrorTypes.MEDIA_ERROR: errorMessage = 'Media error - video format may be unsupported'; // Try to recover from media errors this.hls.recoverMediaError(); return; case Hls.ErrorTypes.OTHER_ERROR: errorMessage = 'Playback error - ' + data.details; break; } this.showError(errorMessage); } tryDirectStreaming() { console.log('Attempting direct streaming fallback'); // Clean up HLS if (this.hls) { this.hls.destroy(); this.hls = null; } // Try direct video streaming const directUrl = `/api/stream/${this.videoHash}`; this.video.src = directUrl; this.video.addEventListener('canplay', () => { console.log('Direct streaming successful'); this.updateVideoInfo(); }); this.video.addEventListener('error', (e) => { console.error('Direct streaming also failed:', e); this.showError('Video playback failed. The file may be corrupted or in an unsupported format.'); }); // Try to play this.video.load(); } showError(message, showExternalPlayerOption = false) { const videoContainer = document.querySelector('.video-container'); let externalPlayerButtons = ''; if (showExternalPlayerOption && this.videoHash) { externalPlayerButtons = `

Use External Player:

For best experience with MKV files, use VLC Media Player or similar external video players.

`; } videoContainer.innerHTML = `
${showExternalPlayerOption ? '🎬' : '⚠️'}

${showExternalPlayerOption ? 'Browser Compatibility Issue' : 'Playback Error'}

${message}

${externalPlayerButtons}
`; } // Utility functions formatTime(seconds) { if (!isFinite(seconds)) return '--:--'; const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); const secs = Math.floor(seconds % 60); if (hours > 0) { return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; } return `${minutes}:${secs.toString().padStart(2, '0')}`; } 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 parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; } showToast(message, type = 'info') { const toastContainer = document.getElementById('toast-container'); const toast = document.createElement('div'); toast.className = `toast ${type}`; toast.textContent = message; toastContainer.appendChild(toast); setTimeout(() => { toast.remove(); }, 3000); } } // Global functions function copyShareLink() { const directLink = document.getElementById('direct-link').value; if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(directLink).then(() => { player.showToast('Share link copied to clipboard!', 'success'); }); } else { // Fallback const input = document.getElementById('direct-link'); input.select(); document.execCommand('copy'); player.showToast('Share link copied to clipboard!', 'success'); } } function downloadVideo() { const urlParams = new URLSearchParams(window.location.search); const videoHash = urlParams.get('hash'); const videoName = urlParams.get('name') || 'video'; if (videoHash) { const url = `/api/download/${videoHash}`; const a = document.createElement('a'); a.href = url; a.download = videoName; document.body.appendChild(a); a.click(); document.body.removeChild(a); } } function getTorrent() { const urlParams = new URLSearchParams(window.location.search); const videoHash = urlParams.get('hash'); const videoName = urlParams.get('name') || 'video'; if (videoHash) { const url = `/api/torrent/${videoHash}`; const a = document.createElement('a'); a.href = url; a.download = `${videoName}.torrent`; document.body.appendChild(a); a.click(); document.body.removeChild(a); } } function openWebSeed() { const urlParams = new URLSearchParams(window.location.search); const videoHash = urlParams.get('hash'); if (videoHash) { const url = `/api/webseed/${videoHash}/`; window.open(url, '_blank'); } } function copyVLCURL() { const urlParams = new URLSearchParams(window.location.search); const videoHash = urlParams.get('hash'); if (videoHash) { const streamURL = `${window.location.origin}/api/stream/${videoHash}`; if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(streamURL).then(() => { showToastMessage('VLC streaming URL copied to clipboard!', 'success'); }); } else { // Fallback const textarea = document.createElement('textarea'); textarea.value = streamURL; document.body.appendChild(textarea); textarea.select(); document.execCommand('copy'); document.body.removeChild(textarea); showToastMessage('VLC streaming URL copied to clipboard!', 'success'); } } } function showToastMessage(message, type = 'info') { const toastContainer = document.getElementById('toast-container'); if (toastContainer) { const toast = document.createElement('div'); toast.className = `toast ${type}`; toast.textContent = message; toastContainer.appendChild(toast); setTimeout(() => { toast.remove(); }, 3000); } else { // Fallback to alert if toast container doesn't exist alert(message); } } function copyToClipboard(elementId) { const element = document.getElementById(elementId); element.select(); document.execCommand('copy'); showToastMessage('Copied to clipboard!', 'success'); } function toggleTheme() { const currentTheme = document.documentElement.getAttribute('data-theme'); const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; document.documentElement.setAttribute('data-theme', newTheme); localStorage.setItem('theme', newTheme); } // P2P toggle function function toggleP2P() { if (!player || !player.webTorrentClient) { showToastMessage('WebTorrent not available in this browser', 'error'); return; } if (player.isP2PEnabled) { player.disableP2P(); } else { player.enableP2P(); } } // Add P2P methods to VideoPlayer class VideoPlayer.prototype.enableP2P = async function() { if (!this.webTorrentClient || this.isP2PEnabled) return; try { const response = await fetch(`/api/webtorrent/${this.videoHash}`); if (!response.ok) throw new Error('Failed to get WebTorrent info'); const data = await response.json(); const magnetURI = data.magnet_uri; showToastMessage('Connecting to P2P network...', 'info'); document.getElementById('p2p-toggle').textContent = '⏳ Connecting...'; this.webTorrentClient.add(magnetURI, (torrent) => { this.currentTorrent = torrent; this.isP2PEnabled = true; // Find video file const file = torrent.files.find(f => f.name.endsWith('.mp4') || f.name.endsWith('.webm') || f.name.endsWith('.mkv') || f.name.endsWith('.avi') ); if (file) { // Prioritize sequential download for streaming file.select(); // Replace video source with P2P stream file.streamTo(this.video); showToastMessage('P2P streaming enabled!', 'success'); document.getElementById('p2p-toggle').textContent = '🔗 Disable P2P'; // Update P2P stats this.updateP2PStats(); this.p2pStatsInterval = setInterval(() => this.updateP2PStats(), 1000); } }); } catch (error) { console.error('P2P enable error:', error); showToastMessage('Failed to enable P2P streaming', 'error'); document.getElementById('p2p-toggle').textContent = '🔗 Enable P2P'; } }; VideoPlayer.prototype.disableP2P = function() { if (!this.isP2PEnabled) return; if (this.currentTorrent) { this.currentTorrent.destroy(); this.currentTorrent = null; } if (this.p2pStatsInterval) { clearInterval(this.p2pStatsInterval); this.p2pStatsInterval = null; } this.isP2PEnabled = false; document.getElementById('p2p-toggle').textContent = '🔗 Enable P2P'; // Reset P2P stats document.getElementById('p2p-peers').textContent = '0'; document.getElementById('p2p-download').textContent = '0 KB/s'; document.getElementById('p2p-upload').textContent = '0 KB/s'; // Revert to direct streaming this.initializeDirectStreaming(); showToastMessage('Switched back to direct streaming', 'info'); }; VideoPlayer.prototype.updateP2PStats = function() { if (!this.currentTorrent) return; document.getElementById('p2p-peers').textContent = this.currentTorrent.numPeers; document.getElementById('p2p-download').textContent = this.formatSpeed(this.currentTorrent.downloadSpeed); document.getElementById('p2p-upload').textContent = this.formatSpeed(this.currentTorrent.uploadSpeed); }; VideoPlayer.prototype.formatSpeed = function(bytes) { if (bytes < 1024) return bytes + ' B/s'; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB/s'; return (bytes / 1024 / 1024).toFixed(1) + ' MB/s'; }; // Initialize player when page loads let player; document.addEventListener('DOMContentLoaded', () => { player = new VideoPlayer(); });