|
|
@@ -0,0 +1,479 @@
|
|
|
+// @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
|