// 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.