enki b3204ea07a
Some checks are pending
CI Pipeline / Run Tests (push) Waiting to run
CI Pipeline / Lint Code (push) Waiting to run
CI Pipeline / Security Scan (push) Waiting to run
CI Pipeline / Build Docker Images (push) Blocked by required conditions
CI Pipeline / E2E Tests (push) Blocked by required conditions
first commit
2025-08-18 00:40:15 -07:00

626 lines
24 KiB
JavaScript

// HLS Video Player with statistics and sharing
class VideoPlayer {
constructor() {
this.hls = null;
this.video = null;
this.videoHash = null;
this.videoName = null;
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);
}
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 = `
<div style="display: flex; align-items: center; justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 400px; border-radius: 12px; color: white;">
<div style="text-align: center; max-width: 600px; padding: 30px;">
<div style="font-size: 4rem; margin-bottom: 20px;">🎬</div>
<h2 style="margin-bottom: 20px; font-size: 1.8rem;">MKV File Detected</h2>
<p style="margin-bottom: 25px; font-size: 1.1rem; line-height: 1.6; opacity: 0.9;">
<strong>Browser Compatibility Notice:</strong><br>
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.
</p>
<div style="background-color: rgba(255,255,255,0.15); padding: 20px; border-radius: 8px; margin: 25px 0; text-align: left;">
<h4 style="margin-bottom: 10px;">🔧 Technical Details:</h4>
<ul style="margin: 0; padding-left: 20px; opacity: 0.9;">
<li><strong>Firefox:</strong> No native MKV support</li>
<li><strong>Chrome:</strong> Partial support, often audio issues</li>
<li><strong>Codec:</strong> Your file likely uses DDP5.1 audio</li>
</ul>
</div>
<h3 style="margin-bottom: 20px;">📥 Available Options:</h3>
<div style="display: flex; gap: 15px; justify-content: center; flex-wrap: wrap; margin-bottom: 25px;">
<button onclick="downloadVideo()" class="action-btn"
style="background-color: #28a745; color: white; padding: 12px 24px; border: none; border-radius: 6px; font-size: 1rem; cursor: pointer; box-shadow: 0 2px 8px rgba(0,0,0,0.2);">
📥 Download File
</button>
<button onclick="copyVLCURL()" class="action-btn"
style="background-color: #ff8c00; color: white; padding: 12px 24px; border: none; border-radius: 6px; font-size: 1rem; cursor: pointer; box-shadow: 0 2px 8px rgba(0,0,0,0.2);">
🎬 Copy VLC URL
</button>
<button onclick="getTorrent()" class="action-btn"
style="background-color: #6f42c1; color: white; padding: 12px 24px; border: none; border-radius: 6px; font-size: 1rem; cursor: pointer; box-shadow: 0 2px 8px rgba(0,0,0,0.2);">
🧲 Get Torrent
</button>
</div>
<div style="background-color: rgba(255,255,255,0.1); padding: 15px; border-radius: 8px; font-size: 0.95rem;">
💡 <strong>Recommended:</strong> Use VLC Media Player, MPV, or similar desktop players for best MKV playback experience.
</div>
</div>
</div>
`;
// 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 = `
<div style="margin-top: 20px; padding-top: 20px; border-top: 1px solid var(--border-color);">
<h4 style="margin-bottom: 15px; color: var(--text-primary);">Use External Player:</h4>
<div style="display: flex; gap: 10px; justify-content: center; flex-wrap: wrap;">
<button onclick="downloadVideo()" class="action-btn"
style="background-color: var(--primary); color: white;">
📥 Download File
</button>
<button onclick="copyVLCURL()" class="action-btn"
style="background-color: var(--success); color: white;">
🎬 Copy VLC URL
</button>
<button onclick="openWebSeed()" class="action-btn"
style="background-color: var(--info); color: white;">
🌐 Open in VLC
</button>
</div>
<p style="margin-top: 15px; font-size: 0.9rem; color: var(--text-secondary);">
For best experience with MKV files, use VLC Media Player or similar external video players.
</p>
</div>
`;
}
videoContainer.innerHTML = `
<div style="display: flex; align-items: center; justify-content: center;
background-color: var(--bg-secondary); color: var(--danger);
min-height: 300px; border-radius: 12px;">
<div style="text-align: center; max-width: 500px; padding: 20px;">
<div style="font-size: 3rem; margin-bottom: 20px;">${showExternalPlayerOption ? '🎬' : '⚠️'}</div>
<h3>${showExternalPlayerOption ? 'Browser Compatibility Issue' : 'Playback Error'}</h3>
<p style="margin-bottom: 20px;">${message}</p>
<button onclick="location.reload()" class="action-btn"
style="margin-right: 10px;">🔄 Retry</button>
${externalPlayerButtons}
</div>
</div>
`;
}
// 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);
}
// Initialize player when page loads
let player;
document.addEventListener('DOMContentLoaded', () => {
player = new VideoPlayer();
});