// @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, container: null, /** * 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(); }, /** * 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()); // Seek slider this.seekSlider.addEventListener('input', () => this.seek()); // Progress bar click to seek this.progressBar.addEventListener('click', (e) => this.progressBarSeek(e)); // Mute button this.muteBtn.addEventListener('click', () => this.toggleMute()); // Volume slider this.volumeSlider.addEventListener('input', () => this.updateVolume()); // Fullscreen button this.fullscreenBtn.addEventListener('click', () => this.toggleFullscreen()); // 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.video.addEventListener('pause', () => { this.updatePlayPauseButton(); this.showControls(); // Show controls when paused }); this.video.addEventListener('seeking', () => { this.showControls(); // Show controls when seeking }); this.video.addEventListener('seeked', () => { this.hideControlsWithTimeout(); // Auto-hide 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.showControls()); this.container.addEventListener('mouseleave', () => this.hideControls()); this.container.addEventListener('mouseenter', () => this.showControls()); } // 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 after a delay in fullscreen mode if video is playing if (isFullscreen && !this.video.paused) { this.hideControlsWithTimeout(); } }, /** * 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'); } // Only reset timeout if video is playing (to allow auto-hide when idle) if (!this.video.paused) { this.resetControlsTimeout(); this.hideControlsWithTimeout(); } }, /** * Hide the controls */ hideControls() { if (!this.video.paused) { this.controls.classList.remove('show'); if (this.topControls) { this.topControls.classList.remove('show'); } } }, /** * 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 }, /** * 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(); } }, 1000); }, }; // @license-end