/** * EasyStream Video Player * Advanced HTML5 video player with HLS support */ class EasyStreamPlayer { constructor(containerId, config) { this.container = document.getElementById(containerId); this.config = config; this.video = null; this.hls = null; this.currentQuality = 'auto'; this.isFullscreen = false; this.controlsTimeout = null; this.progressUpdateInterval = null; this.init(); } init() { this.video = this.container.querySelector('.video-element'); this.setupEventListeners(); this.loadVideo(); this.setupProgressTracking(); // Load HLS.js if needed if (this.config.hlsSupported && !window.Hls) { this.loadHLSLibrary(); } } setupEventListeners() { const controls = this.container.querySelector('.video-controls'); const bigPlayBtn = this.container.querySelector('.big-play-button'); // Video events this.video.addEventListener('loadstart', () => this.showLoading()); this.video.addEventListener('canplay', () => this.hideLoading()); this.video.addEventListener('play', () => this.onPlay()); this.video.addEventListener('pause', () => this.onPause()); this.video.addEventListener('timeupdate', () => this.updateProgress()); this.video.addEventListener('durationchange', () => this.updateDuration()); this.video.addEventListener('progress', () => this.updateBuffer()); this.video.addEventListener('error', (e) => this.onError(e)); this.video.addEventListener('ended', () => this.onEnded()); // Control events this.setupControlEvents(); // Big play button bigPlayBtn.addEventListener('click', () => this.togglePlay()); // Container hover for controls this.container.addEventListener('mouseenter', () => this.showControls()); this.container.addEventListener('mouseleave', () => this.hideControlsDelayed()); this.container.addEventListener('mousemove', () => this.showControls()); // Touch events for mobile this.setupTouchEvents(); // Keyboard shortcuts this.setupKeyboardShortcuts(); // Fullscreen events document.addEventListener('fullscreenchange', () => this.onFullscreenChange()); document.addEventListener('webkitfullscreenchange', () => this.onFullscreenChange()); document.addEventListener('mozfullscreenchange', () => this.onFullscreenChange()); document.addEventListener('MSFullscreenChange', () => this.onFullscreenChange()); } setupControlEvents() { const controls = this.container.querySelector('.video-controls'); // Play/Pause button const playPauseBtn = controls.querySelector('.play-pause-btn'); playPauseBtn.addEventListener('click', () => this.togglePlay()); // Progress bar const progressBar = controls.querySelector('.progress-bar'); progressBar.addEventListener('click', (e) => this.seekToPosition(e)); progressBar.addEventListener('mousedown', (e) => this.startSeeking(e)); // Volume controls const volumeBtn = controls.querySelector('.volume-btn'); const volumeBar = controls.querySelector('.volume-bar'); volumeBtn.addEventListener('click', () => this.toggleMute()); volumeBar.addEventListener('click', (e) => this.setVolume(e)); // Quality selector const qualityOptions = controls.querySelectorAll('.quality-option'); qualityOptions.forEach(option => { option.addEventListener('click', () => this.changeQuality(option.dataset.quality)); }); // Speed selector const speedOptions = controls.querySelectorAll('.speed-option'); speedOptions.forEach(option => { option.addEventListener('click', () => this.changeSpeed(parseFloat(option.dataset.speed))); }); // Fullscreen button const fullscreenBtn = controls.querySelector('.fullscreen-btn'); fullscreenBtn.addEventListener('click', () => this.toggleFullscreen()); // Picture-in-Picture button const pipBtn = controls.querySelector('.pip-btn'); if (pipBtn && 'pictureInPictureEnabled' in document) { pipBtn.style.display = 'block'; pipBtn.addEventListener('click', () => this.togglePictureInPicture()); } } setupTouchEvents() { let touchStartX = 0; let touchStartY = 0; let touchStartTime = 0; this.container.addEventListener('touchstart', (e) => { touchStartX = e.touches[0].clientX; touchStartY = e.touches[0].clientY; touchStartTime = this.video.currentTime; }); this.container.addEventListener('touchmove', (e) => { e.preventDefault(); const touchX = e.touches[0].clientX; const touchY = e.touches[0].clientY; const deltaX = touchX - touchStartX; const deltaY = touchY - touchStartY; // Horizontal swipe for seeking if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > 50) { const seekAmount = (deltaX / this.container.offsetWidth) * this.video.duration; const newTime = Math.max(0, Math.min(this.video.duration, touchStartTime + seekAmount)); this.video.currentTime = newTime; } // Vertical swipe for volume (left side) or brightness (right side) if (Math.abs(deltaY) > Math.abs(deltaX) && Math.abs(deltaY) > 50) { if (touchStartX < this.container.offsetWidth / 2) { // Left side - volume const volumeChange = -deltaY / this.container.offsetHeight; const newVolume = Math.max(0, Math.min(1, this.video.volume + volumeChange)); this.video.volume = newVolume; this.updateVolumeDisplay(); } } }); this.container.addEventListener('touchend', () => { this.showControls(); this.hideControlsDelayed(); }); } setupKeyboardShortcuts() { document.addEventListener('keydown', (e) => { if (!this.isPlayerFocused()) return; switch (e.code) { case 'Space': e.preventDefault(); this.togglePlay(); break; case 'ArrowLeft': e.preventDefault(); this.seek(-10); break; case 'ArrowRight': e.preventDefault(); this.seek(10); break; case 'ArrowUp': e.preventDefault(); this.changeVolume(0.1); break; case 'ArrowDown': e.preventDefault(); this.changeVolume(-0.1); break; case 'KeyM': e.preventDefault(); this.toggleMute(); break; case 'KeyF': e.preventDefault(); this.toggleFullscreen(); break; case 'Escape': if (this.isFullscreen) { this.exitFullscreen(); } break; } }); } loadVideo() { this.showLoading(); // Get stream information fetch(`/stream.php?v=${this.config.videoKey}`) .then(response => response.json()) .then(data => { if (data.success) { this.setupVideoSources(data); // Set start time if specified if (this.config.startTime > 0) { this.video.currentTime = this.config.startTime; } // Autoplay if enabled if (this.config.autoplay) { this.play(); } } else { this.showError(data.error || 'Failed to load video'); } }) .catch(error => { console.error('Error loading video:', error); this.showError('Failed to load video'); }); } setupVideoSources(streamData) { // Clear existing sources this.video.innerHTML = ''; if (streamData.streams.hls && this.supportsHLS()) { // Use HLS for adaptive streaming this.setupHLS(streamData.streams.hls.url); } else if (streamData.streams.progressive) { // Use progressive download this.setupProgressive(streamData.streams.progressive); } else { this.showError('No compatible video format available'); return; } // Update quality options this.updateQualityOptions(streamData.streams); } setupHLS(hlsUrl) { if (window.Hls && Hls.isSupported()) { this.hls = new Hls({ enableWorker: true, lowLatencyMode: false, backBufferLength: 90 }); this.hls.loadSource(hlsUrl); this.hls.attachMedia(this.video); this.hls.on(Hls.Events.MANIFEST_PARSED, () => { console.log('HLS manifest loaded'); this.hideLoading(); }); this.hls.on(Hls.Events.ERROR, (event, data) => { console.error('HLS error:', data); if (data.fatal) { this.showError('Video streaming error'); } }); } else if (this.video.canPlayType('application/vnd.apple.mpegurl')) { // Native HLS support (Safari) this.video.src = hlsUrl; } else { this.showError('HLS not supported'); } } setupProgressive(progressiveStreams) { // Sort qualities by resolution (highest first) const sortedQualities = Object.entries(progressiveStreams) .sort((a, b) => this.getResolutionValue(b[0]) - this.getResolutionValue(a[0])); // Add source elements sortedQualities.forEach(([quality, stream]) => { const source = document.createElement('source'); source.src = stream.url; source.type = stream.type; source.setAttribute('data-quality', quality); this.video.appendChild(source); }); // Set default quality if (sortedQualities.length > 0) { this.video.src = sortedQualities[0][1].url; this.currentQuality = sortedQualities[0][0]; } } loadHLSLibrary() { const script = document.createElement('script'); script.src = 'https://cdn.jsdelivr.net/npm/hls.js@latest'; script.onload = () => { console.log('HLS.js loaded'); this.loadVideo(); }; document.head.appendChild(script); } supportsHLS() { return window.Hls && Hls.isSupported() || this.video.canPlayType('application/vnd.apple.mpegurl'); } togglePlay() { if (this.video.paused) { this.play(); } else { this.pause(); } } play() { const playPromise = this.video.play(); if (playPromise !== undefined) { playPromise .then(() => { console.log('Video started playing'); }) .catch(error => { console.error('Error playing video:', error); this.showError('Unable to play video'); }); } } pause() { this.video.pause(); } seek(seconds) { const newTime = Math.max(0, Math.min(this.video.duration, this.video.currentTime + seconds)); this.video.currentTime = newTime; } seekToPosition(e) { const progressBar = e.currentTarget; const rect = progressBar.getBoundingClientRect(); const position = (e.clientX - rect.left) / rect.width; const newTime = position * this.video.duration; this.video.currentTime = newTime; } changeVolume(delta) { const newVolume = Math.max(0, Math.min(1, this.video.volume + delta)); this.video.volume = newVolume; this.updateVolumeDisplay(); } toggleMute() { this.video.muted = !this.video.muted; this.updateVolumeDisplay(); } setVolume(e) { const volumeBar = e.currentTarget; const rect = volumeBar.getBoundingClientRect(); const position = 1 - ((e.clientY - rect.top) / rect.height); const newVolume = Math.max(0, Math.min(1, position)); this.video.volume = newVolume; this.video.muted = false; this.updateVolumeDisplay(); } changeQuality(quality) { if (quality === this.currentQuality) return; const currentTime = this.video.currentTime; const wasPlaying = !this.video.paused; if (quality === 'auto' && this.hls) { this.hls.currentLevel = -1; // Auto quality } else if (this.hls) { // Find HLS level for quality const levels = this.hls.levels; const targetLevel = levels.findIndex(level => level.height === this.getResolutionValue(quality) ); if (targetLevel !== -1) { this.hls.currentLevel = targetLevel; } } else { // Progressive quality change const sources = this.video.querySelectorAll('source'); const targetSource = Array.from(sources).find(source => source.getAttribute('data-quality') === quality ); if (targetSource) { this.video.src = targetSource.src; this.video.currentTime = currentTime; if (wasPlaying) { this.video.play(); } } } this.currentQuality = quality; this.updateQualityDisplay(); } changeSpeed(speed) { this.video.playbackRate = speed; this.updateSpeedDisplay(); } toggleFullscreen() { if (this.isFullscreen) { this.exitFullscreen(); } else { this.enterFullscreen(); } } enterFullscreen() { const element = this.container; if (element.requestFullscreen) { element.requestFullscreen(); } else if (element.webkitRequestFullscreen) { element.webkitRequestFullscreen(); } else if (element.mozRequestFullScreen) { element.mozRequestFullScreen(); } else if (element.msRequestFullscreen) { element.msRequestFullscreen(); } } exitFullscreen() { if (document.exitFullscreen) { document.exitFullscreen(); } else if (document.webkitExitFullscreen) { document.webkitExitFullscreen(); } else if (document.mozCancelFullScreen) { document.mozCancelFullScreen(); } else if (document.msExitFullscreen) { document.msExitFullscreen(); } } togglePictureInPicture() { if (document.pictureInPictureElement) { document.exitPictureInPicture(); } else { this.video.requestPictureInPicture(); } } // Event handlers onPlay() { this.updatePlayPauseButton(); this.hideBigPlayButton(); this.startProgressTracking(); } onPause() { this.updatePlayPauseButton(); this.showBigPlayButton(); this.stopProgressTracking(); } onEnded() { this.showBigPlayButton(); this.stopProgressTracking(); this.updateProgress(); } onError(e) { console.error('Video error:', e); this.showError('Video playback error'); } onFullscreenChange() { this.isFullscreen = !!(document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement); this.updateFullscreenButton(); } // UI Updates updatePlayPauseButton() { const playIcon = this.container.querySelector('.play-pause-btn .icon-play'); const pauseIcon = this.container.querySelector('.play-pause-btn .icon-pause'); if (this.video.paused) { playIcon.style.display = 'inline'; pauseIcon.style.display = 'none'; } else { playIcon.style.display = 'none'; pauseIcon.style.display = 'inline'; } } updateProgress() { if (!this.video.duration) return; const progress = (this.video.currentTime / this.video.duration) * 100; const progressPlayed = this.container.querySelector('.progress-played'); const progressHandle = this.container.querySelector('.progress-handle'); progressPlayed.style.width = progress + '%'; progressHandle.style.left = progress + '%'; this.updateTimeDisplay(); } updateBuffer() { if (!this.video.duration || !this.video.buffered.length) return; const buffered = this.video.buffered.end(this.video.buffered.length - 1); const progress = (buffered / this.video.duration) * 100; const progressBuffer = this.container.querySelector('.progress-buffer'); progressBuffer.style.width = progress + '%'; } updateDuration() { this.updateTimeDisplay(); } updateTimeDisplay() { const currentTimeEl = this.container.querySelector('.current-time'); const durationEl = this.container.querySelector('.duration'); currentTimeEl.textContent = this.formatTime(this.video.currentTime); durationEl.textContent = this.formatTime(this.video.duration || 0); } updateVolumeDisplay() { const volumeFill = this.container.querySelector('.volume-fill'); const volumeIcon = this.container.querySelector('.volume-btn i'); const volume = this.video.muted ? 0 : this.video.volume; volumeFill.style.height = (volume * 100) + '%'; // Update volume icon if (this.video.muted || volume === 0) { volumeIcon.className = 'icon-volume-off'; } else { volumeIcon.className = 'icon-volume-up'; } } updateQualityDisplay() { const qualityLabel = this.container.querySelector('.quality-label'); const qualityOptions = this.container.querySelectorAll('.quality-option'); qualityLabel.textContent = this.currentQuality === 'auto' ? 'Auto' : this.currentQuality; qualityOptions.forEach(option => { option.classList.toggle('active', option.dataset.quality === this.currentQuality); }); } updateSpeedDisplay() { const speedLabel = this.container.querySelector('.speed-label'); const speedOptions = this.container.querySelectorAll('.speed-option'); speedLabel.textContent = this.video.playbackRate + 'x'; speedOptions.forEach(option => { option.classList.toggle('active', parseFloat(option.dataset.speed) === this.video.playbackRate); }); } updateFullscreenButton() { const fullscreenIcon = this.container.querySelector('.fullscreen-btn .icon-fullscreen'); const exitFullscreenIcon = this.container.querySelector('.fullscreen-btn .icon-fullscreen-exit'); if (this.isFullscreen) { fullscreenIcon.style.display = 'none'; exitFullscreenIcon.style.display = 'inline'; } else { fullscreenIcon.style.display = 'inline'; exitFullscreenIcon.style.display = 'none'; } } // Controls visibility showControls() { const controls = this.container.querySelector('.video-controls'); controls.classList.add('show'); if (this.controlsTimeout) { clearTimeout(this.controlsTimeout); } } hideControlsDelayed() { if (this.controlsTimeout) { clearTimeout(this.controlsTimeout); } this.controlsTimeout = setTimeout(() => { const controls = this.container.querySelector('.video-controls'); controls.classList.remove('show'); }, 3000); } showBigPlayButton() { const bigPlayBtn = this.container.querySelector('.big-play-button'); bigPlayBtn.style.display = 'flex'; } hideBigPlayButton() { const bigPlayBtn = this.container.querySelector('.big-play-button'); bigPlayBtn.style.display = 'none'; } showLoading() { const loading = this.container.querySelector('.video-loading'); loading.style.display = 'block'; } hideLoading() { const loading = this.container.querySelector('.video-loading'); loading.style.display = 'none'; } showError(message) { const error = this.container.querySelector('.video-error'); const errorMessage = error.querySelector('.error-message'); errorMessage.textContent = message; error.style.display = 'flex'; // Retry button const retryBtn = error.querySelector('.retry-btn'); retryBtn.onclick = () => { error.style.display = 'none'; this.loadVideo(); }; } // Progress tracking for analytics setupProgressTracking() { if (!this.config.userId) return; // Track progress every 10 seconds this.progressUpdateInterval = setInterval(() => { this.updateWatchProgress(); }, 10000); // Track on pause/seek this.video.addEventListener('pause', () => this.updateWatchProgress()); this.video.addEventListener('seeked', () => this.updateWatchProgress()); } startProgressTracking() { if (this.progressUpdateInterval) return; this.setupProgressTracking(); } stopProgressTracking() { if (this.progressUpdateInterval) { clearInterval(this.progressUpdateInterval); this.progressUpdateInterval = null; } this.updateWatchProgress(); } updateWatchProgress() { if (!this.config.userId || !this.video.duration) return; const data = { video_key: this.config.videoKey, current_time: this.video.currentTime, duration: this.video.duration, csrf_token: this.config.csrfToken }; fetch('/api/video/progress', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(data) }).catch(error => { console.error('Error updating progress:', error); }); } // Utility methods formatTime(seconds) { if (!seconds || isNaN(seconds)) return '0:00'; 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')}`; } else { return `${minutes}:${secs.toString().padStart(2, '0')}`; } } getResolutionValue(quality) { const resolutions = { '1080p': 1080, '720p': 720, '480p': 480, '360p': 360, '240p': 240 }; return resolutions[quality] || 0; } isPlayerFocused() { return this.container.contains(document.activeElement) || document.activeElement === document.body; } updateQualityOptions(streams) { const qualityMenu = this.container.querySelector('.quality-menu'); // Clear existing options except auto const autoOption = qualityMenu.querySelector('[data-quality="auto"]'); qualityMenu.innerHTML = ''; qualityMenu.appendChild(autoOption); // Add available qualities if (streams.progressive) { Object.keys(streams.progressive).forEach(quality => { const option = document.createElement('div'); option.className = 'quality-option'; option.dataset.quality = quality; option.textContent = this.getQualityLabel(quality); option.addEventListener('click', () => this.changeQuality(quality)); qualityMenu.appendChild(option); }); } } getQualityLabel(quality) { const labels = { '1080p': '1080p HD', '720p': '720p HD', '480p': '480p', '360p': '360p', '240p': '240p' }; return labels[quality] || quality; } } // Export for use in other scripts window.EasyStreamPlayer = EasyStreamPlayer;