// @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later /** * CustomVideoControls object to handle custom video player functionality */ const CustomVideoControls = { video: null, controls: null, topControls: null, playPauseBtn: null, seekSlider: null, progressBar: null, progressFilled: null, progressBuffered: null, timeDisplay: null, muteBtn: null, volumeSlider: null, fullscreenBtn: null, controlsTimeout: null, mouseTimeout: null, container: null, isMouseOverContainer: false, /** * Initialize the custom video controls */ init() { // Get DOM elements this.video = document.getElementById('player'); this.controls = document.getElementById('custom-controls'); this.topControls = document.getElementById('controls'); this.playPauseBtn = document.getElementById('play-pause-btn'); this.seekSlider = document.getElementById('seek-slider'); this.progressBar = document.getElementById('progress-bar'); this.progressFilled = document.getElementById('progress-filled'); this.progressBuffered = document.getElementById('progress-buffered'); this.timeDisplay = document.getElementById('time-display'); this.muteBtn = document.getElementById('mute-btn'); this.volumeSlider = document.getElementById('volume-slider'); this.fullscreenBtn = document.getElementById('fullscreen-btn'); if (!this.video || !this.controls) { console.error('Custom video controls: Required elements not found'); return; } // Create a container for video and controls if it doesn't exist this.createVideoContainer(); // Set up event listeners this.setupEventListeners(); // Initialize controls state this.updatePlayPauseButton(); this.updateVolume(); this.updateTimeDisplay(); // Show controls initially this.showControls(); // Set up mouse movement detection for auto-hiding controls this.setupAutoHide(); // Set up mouse inactivity detection this.setupMouseInactivityDetection(); // Set up focus handling for top controls this.setupTopControlsFocusHandling(); }, /** * Create a container wrapper for video and controls for proper fullscreen handling */ createVideoContainer() { // Container already exists in HTML, just get reference this.container = document.getElementById('video-container'); if (!this.container) { console.error('Video container not found in HTML'); return; } }, /** * Set up all event listeners for the custom controls */ setupEventListeners() { // Play/Pause button this.playPauseBtn.addEventListener('click', () => { this.togglePlayPause(); this.playPauseBtn.blur(); }); // Seek slider this.seekSlider.addEventListener('input', () => { this.seek(); }); this.seekSlider.addEventListener('change', () => { this.seekSlider.blur(); }); // Progress bar click to seek this.progressBar.addEventListener('click', (e) => { this.progressBarSeek(e); e.target.blur(); }); // Mute button this.muteBtn.addEventListener('click', () => { this.toggleMute(); this.muteBtn.blur(); }); // Volume slider this.volumeSlider.addEventListener('input', () => { this.updateVolume(); }); this.volumeSlider.addEventListener('change', () => { this.volumeSlider.blur(); }); // Fullscreen button this.fullscreenBtn.addEventListener('click', () => { this.toggleFullscreen(); this.fullscreenBtn.blur(); }); // Video click to toggle play/pause this.video.addEventListener('click', (e) => { // Prevent triggering if clicking on controls or if it's a right-click if (e.target === this.video && e.button === 0) { this.togglePlayPause(); } }); // Video events this.video.addEventListener('timeupdate', () => this.updateProgress()); this.video.addEventListener('loadedmetadata', () => this.updateTimeDisplay()); this.video.addEventListener('volumechange', () => this.updateVolumeDisplay()); this.video.addEventListener('play', () => { this.updatePlayPauseButton(); this.hideControlsWithTimeout(); // Auto-hide controls when playing this.hideCursorWithTimeout(); // Auto-hide cursor when playing }); this.video.addEventListener('pause', () => { this.updatePlayPauseButton(); this.showControls(); // Show controls when paused this.showCursor(); // Show cursor when paused this.resetMouseTimeout(); // Clear cursor timeout when paused }); this.video.addEventListener('seeking', () => { this.showControls(); // Show controls when seeking }); this.video.addEventListener('seeked', () => { this.hideControlsWithTimeout(); // Auto-hide after seeking this.hideCursorWithTimeout(); // Auto-hide cursor after seeking }); // Keyboard controls document.addEventListener('keydown', (e) => this.handleKeyboard(e)); // Controls visibility this.controls.addEventListener('mouseenter', () => this.showControls()); this.controls.addEventListener('mouseleave', () => this.hideControls()); this.video.addEventListener('mouseenter', () => this.showControls()); this.video.addEventListener('mouseleave', () => this.hideControls()); this.video.addEventListener('mousemove', () => this.showControls()); // Add mouse movement detection for the entire container if (this.container) { this.container.addEventListener('mousemove', () => this.handleMouseMove()); this.container.addEventListener('mouseleave', () => this.handleMouseLeave()); this.container.addEventListener('mouseenter', () => this.handleMouseEnter()); } // Touch events for mobile support this.video.addEventListener('touchstart', (e) => { this.showControls(); // Prevent default to avoid interfering with video controls if (e.touches.length === 1) { const touch = e.touches[0]; const rect = this.video.getBoundingClientRect(); // Only toggle play/pause if tapping in the center area if (touch.clientX > rect.left + rect.width * 0.3 && touch.clientX < rect.left + rect.width * 0.7 && touch.clientY > rect.top + rect.height * 0.3 && touch.clientY < rect.top + rect.height * 0.7) { this.togglePlayPause(); } } }); this.controls.addEventListener('touchstart', () => { this.showControls(); }); // Handle window resize for responsive behavior window.addEventListener('resize', () => { this.handleResize(); }); // Fullscreen change events document.addEventListener('fullscreenchange', () => this.handleFullscreenChange()); document.addEventListener('webkitfullscreenchange', () => this.handleFullscreenChange()); document.addEventListener('mozfullscreenchange', () => this.handleFullscreenChange()); document.addEventListener('MSFullscreenChange', () => this.handleFullscreenChange()); }, /** * Toggle between play and pause */ togglePlayPause() { if (this.video.paused) { this.video.play(); } else { this.video.pause(); } }, /** * Update the play/pause button display */ updatePlayPauseButton() { if (this.video.paused) { this.playPauseBtn.textContent = '▶'; this.playPauseBtn.setAttribute('aria-label', 'Play'); } else { this.playPauseBtn.textContent = '❚❚'; this.playPauseBtn.setAttribute('aria-label', 'Pause'); } }, /** * Seek to the position specified by the seek slider */ seek() { const seekTime = this.seekSlider.value * this.video.duration / 100; this.video.currentTime = seekTime; }, /** * Handle clicking on the progress bar to seek */ progressBarSeek(e) { const rect = this.progressBar.getBoundingClientRect(); const pos = (e.clientX - rect.left) / rect.width; this.video.currentTime = pos * this.video.duration; }, /** * Update the progress bar and time display */ updateProgress() { const percentage = (this.video.currentTime / this.video.duration) * 100; this.progressFilled.style.width = `${percentage}%`; this.seekSlider.value = percentage; // Update buffer indicator if (this.video.buffered.length > 0) { const bufferedEnd = this.video.buffered.end(this.video.buffered.length - 1); const bufferedPercentage = (bufferedEnd / this.video.duration) * 100; this.progressBuffered.style.width = `${bufferedPercentage}%`; } this.updateTimeDisplay(); }, /** * Update the time display */ updateTimeDisplay() { const currentTime = this.formatTime(this.video.currentTime); const duration = this.formatTime(this.video.duration); this.timeDisplay.textContent = `${currentTime} / ${duration}`; }, /** * Format time in seconds to MM:SS or HH:MM:SS format */ formatTime(seconds) { if (isNaN(seconds)) return '0:00'; const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); const s = Math.floor(seconds % 60); if (h > 0) { return `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`; } else { return `${m}:${s.toString().padStart(2, '0')}`; } }, /** * Toggle mute state */ toggleMute() { this.video.muted = !this.video.muted; }, /** * Update volume based on slider value */ updateVolume() { this.video.volume = this.volumeSlider.value; }, /** * Update volume display (mute button) */ updateVolumeDisplay() { if (this.video.muted || this.video.volume === 0) { this.muteBtn.textContent = '🔇'; this.muteBtn.setAttribute('aria-label', 'Unmute'); } else if (this.video.volume < 0.5) { this.muteBtn.textContent = '🔉'; this.muteBtn.setAttribute('aria-label', 'Mute'); } else { this.muteBtn.textContent = '🔊'; this.muteBtn.setAttribute('aria-label', 'Mute'); } }, /** * Toggle fullscreen mode */ toggleFullscreen() { if (!document.fullscreenElement) { // Request fullscreen for the container instead of just the video if (this.container.requestFullscreen) { this.container.requestFullscreen(); } else if (this.container.webkitRequestFullscreen) { /* Safari */ this.container.webkitRequestFullscreen(); } else if (this.container.mozRequestFullScreen) { /* Firefox */ this.container.mozRequestFullScreen(); } else if (this.container.msRequestFullscreen) { /* IE11 */ this.container.msRequestFullscreen(); } } else { if (document.exitFullscreen) { document.exitFullscreen(); } else if (document.webkitExitFullscreen) { /* Safari */ document.webkitExitFullscreen(); } else if (document.mozCancelFullScreen) { /* Firefox */ document.mozCancelFullScreen(); } else if (document.msExitFullscreen) { /* IE11 */ document.msExitFullscreen(); } } }, /** * Handle fullscreen change events */ handleFullscreenChange() { const isFullscreen = !!(document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement); if (isFullscreen) { // Adjust controls for fullscreen this.controls.style.position = 'absolute'; this.controls.style.bottom = '0'; this.controls.style.left = '0'; this.controls.style.right = '0'; this.controls.style.zIndex = '2147483647'; // Maximum z-index // Make video fill the container this.video.style.width = '100%'; this.video.style.height = '100%'; this.video.style.objectFit = 'contain'; } else { // Reset styles when exiting fullscreen this.controls.style.position = ''; this.controls.style.bottom = ''; this.controls.style.left = ''; this.controls.style.right = ''; this.controls.style.zIndex = ''; this.video.style.width = ''; this.video.style.height = ''; this.video.style.objectFit = ''; } // Always show controls when entering/exiting fullscreen this.showControls(); // Auto-hide controls and cursor after a delay in fullscreen mode if video is playing if (isFullscreen && !this.video.paused) { this.hideControlsWithTimeout(); this.hideCursorWithTimeout(); } }, /** * Handle keyboard controls */ handleKeyboard(e) { // Only handle keys when video is the active element or no input is focused if (document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA') { return; } switch (e.key) { case ' ': case 'Spacebar': e.preventDefault(); this.togglePlayPause(); break; case 'ArrowLeft': e.preventDefault(); this.video.currentTime -= 5; break; case 'ArrowRight': e.preventDefault(); this.video.currentTime += 5; break; case 'ArrowUp': e.preventDefault(); this.video.volume = Math.min(1, this.video.volume + 0.1); this.volumeSlider.value = this.video.volume; break; case 'ArrowDown': e.preventDefault(); this.video.volume = Math.max(0, this.video.volume - 0.1); this.volumeSlider.value = this.video.volume; break; case 'm': case 'M': e.preventDefault(); this.toggleMute(); break; case 'f': case 'F': e.preventDefault(); this.toggleFullscreen(); break; } }, /** * Show the controls */ showControls() { this.controls.classList.add('show'); if (this.topControls) { this.topControls.classList.add('show'); } this.showCursor(); // Only reset timeout if video is playing (to allow auto-hide when idle) if (!this.video.paused) { this.resetControlsTimeout(); this.hideControlsWithTimeout(); this.resetMouseTimeout(); this.hideCursorWithTimeout(); } }, /** * Hide the controls */ hideControls() { if (!this.video.paused) { this.controls.classList.remove('show'); if (this.topControls) { this.topControls.classList.remove('show'); } this.hideCursor(); } }, /** * Show the cursor */ showCursor() { if (this.container) { this.container.style.cursor = 'auto'; } }, /** * Hide the cursor */ hideCursor() { if (this.container && !this.video.paused) { this.container.style.cursor = 'none'; } }, /** * Hide cursor with a timeout */ hideCursorWithTimeout() { this.resetMouseTimeout(); this.mouseTimeout = setTimeout(() => { this.hideCursor(); }, 3000); // 3 seconds }, /** * Reset the mouse timeout */ resetMouseTimeout() { if (this.mouseTimeout) { clearTimeout(this.mouseTimeout); this.mouseTimeout = null; } }, /** * Handle mouse movement inside the container */ handleMouseMove() { this.showControls(); if (!this.video.paused) { this.resetMouseTimeout(); this.hideCursorWithTimeout(); } }, /** * Handle mouse entering the container */ handleMouseEnter() { this.isMouseOverContainer = true; this.showControls(); }, /** * Handle mouse leaving the container */ handleMouseLeave() { this.isMouseOverContainer = false; this.hideControls(); }, /** * Hide controls with a timeout */ hideControlsWithTimeout() { this.resetControlsTimeout(); this.controlsTimeout = setTimeout(() => { this.hideControls(); }, 3000); // Changed to 3 seconds }, /** * Reset the controls timeout */ resetControlsTimeout() { if (this.controlsTimeout) { clearTimeout(this.controlsTimeout); this.controlsTimeout = null; } }, /** * Set up auto-hide functionality for controls */ setupAutoHide() { // Note: These event listeners are already added in setupEventListeners() // so we don't need to add them again here to prevent duplicate execution }, /** * Set up mouse inactivity detection */ setupMouseInactivityDetection() { // Initial state - show cursor this.showCursor(); // Set up initial timeout if video is playing if (!this.video.paused) { this.hideCursorWithTimeout(); } }, /** * Handle window resize events */ handleResize() { // Show controls during resize to ensure they're visible this.showControls(); // Auto-hide after resize is complete clearTimeout(this.resizeTimeout); this.resizeTimeout = setTimeout(() => { if (!this.video.paused) { this.hideControlsWithTimeout(); this.hideCursorWithTimeout(); } }, 1000); }, /** * Set up focus handling for top controls */ setupTopControlsFocusHandling() { // Get top control buttons and select const infoBtn = document.querySelector('button[onclick="showInfo()"]'); const shareBtn = document.querySelector('button[onclick="shareVideo()"]'); const downloadBtn = document.querySelector('button[onclick="downloadVideo()"]'); const qualitySelector = document.getElementById('quality-selector'); // Add blur() calls to top control buttons if (infoBtn) { infoBtn.addEventListener('click', () => { infoBtn.blur(); }); } if (shareBtn) { shareBtn.addEventListener('click', () => { shareBtn.blur(); }); } if (downloadBtn) { downloadBtn.addEventListener('click', () => { downloadBtn.blur(); }); } if (qualitySelector) { qualitySelector.addEventListener('change', () => { qualitySelector.blur(); }); } }, }; // @license-end