import videojs from 'video.js'; // import './SeekIndicator.css'; const Component = videojs.getComponent('Component'); // Custom Seek Indicator Component for showing visual feedback during arrow key seeking class SeekIndicator extends Component { constructor(player, options) { super(player, options); this.seekAmount = options.seekAmount || 5; // Default seek amount in seconds this.isEmbedPlayer = options.isEmbedPlayer || false; // Store embed mode flag this.showTimeout = null; // Detect touch devices - if touch is supported, native browser controls will handle icons this.isTouchDevice = this.detectTouchDevice(); } /** * Detect if the device supports touch * @returns {boolean} True if touch is supported */ detectTouchDevice() { return 'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0; } createEl() { const el = super.createEl('div', { className: 'vjs-seek-indicator', }); // Create the indicator content el.innerHTML = `
`; // Initially hide the indicator completely el.style.display = 'none'; el.style.opacity = '0'; el.style.visibility = 'hidden'; return el; } /** * Show seek indicator with direction and amount * @param {string} direction - 'forward', 'backward', 'play', or 'pause' * @param {number} seconds - Number of seconds to seek (only used for forward/backward) */ show(direction, seconds = this.seekAmount) { // Skip showing icons on touch devices as native browser controls handle them /* if (this.isTouchDevice) { return; } */ const el = this.el(); const iconEl = el.querySelector('.vjs-seek-indicator-icon'); const textEl = el.querySelector('.vjs-seek-indicator-text'); // Clear any existing timeout if (this.showTimeout) { clearTimeout(this.showTimeout); } // Get responsive size based on screen width for all directions const isMobile = window.innerWidth <= 480; const isTablet = window.innerWidth <= 768 && window.innerWidth > 480; let circleSize, iconSize, textSize; if (isMobile) { circleSize = '50px'; iconSize = '20'; textSize = '8px'; } else if (isTablet) { circleSize = '60px'; iconSize = '22'; textSize = '9px'; } else { circleSize = '80px'; iconSize = '24'; textSize = '10px'; } // Set content based on direction - YouTube-style circular design if (direction === 'forward') { iconEl.innerHTML = `
${seconds} seconds
`; } else if (direction === 'backward') { iconEl.innerHTML = `
${seconds} seconds
`; } else if (direction === 'play') { iconEl.innerHTML = `
`; textEl.textContent = 'Play'; } else if (direction === 'pause' || direction === 'pause-mobile') { iconEl.innerHTML = `
`; textEl.textContent = 'Pause'; } else if (direction === 'copy-url') { iconEl.innerHTML = `
`; textEl.textContent = ''; } else if (direction === 'copy-embed') { iconEl.innerHTML = `
`; textEl.textContent = ''; } // Clear any text content in the text element textEl.textContent = ''; // Position relative to video player container, not viewport el.style.cssText = ` position: absolute !important; top: 50% !important; left: 50% !important; transform: translate(-50%, -50%) !important; z-index: 10000 !important; display: flex !important; align-items: center !important; justify-content: center !important; visibility: visible !important; opacity: 1 !important; pointer-events: none !important; width: auto !important; height: auto !important; margin: 0 !important; padding: 0 !important; `; // Auto-hide timing based on action type if (direction === 'forward' || direction === 'backward') { // Seek operations: 1 second this.showTimeout = setTimeout(() => { this.hide(); }, 1000); } else if (direction === 'play' || direction === 'pause' || direction === 'pause-mobile') { // Play/pause operations: 500ms this.showTimeout = setTimeout(() => { this.hide(); }, 500); } else if (direction === 'copy-url' || direction === 'copy-embed') { // Copy operations: 500ms (same as play/pause) this.showTimeout = setTimeout(() => { this.hide(); }, 500); } } /** * Show pause icon for mobile (uses 500ms from main show method) */ showMobilePauseIcon() { // Skip showing icons on touch devices as native browser controls handle them if (this.isTouchDevice) { return; } this.show('pause-mobile'); // This will auto-hide after 500ms // Make the icon clickable for mobile const el = this.el(); el.style.pointerEvents = 'auto !important'; // Add click handler for the center icon const handleCenterIconClick = (e) => { e.preventDefault(); e.stopPropagation(); if (this.player().paused()) { this.player().play(); } else { this.player().pause(); } // Hide immediately after click this.hide(); }; el.addEventListener('click', handleCenterIconClick); el.addEventListener('touchend', handleCenterIconClick); // Store handlers for cleanup this.mobileClickHandler = handleCenterIconClick; } /** * Hide mobile pause icon and clean up */ hideMobileIcon() { const el = this.el(); // Remove click handlers const allClickHandlers = el.cloneNode(true); el.parentNode.replaceChild(allClickHandlers, el); // Reset pointer events allClickHandlers.style.pointerEvents = 'none !important'; // Hide the icon this.hide(); // Clear timeout if (this.mobileTimeout) { clearTimeout(this.mobileTimeout); this.mobileTimeout = null; } } /** * Hide the seek indicator */ hide() { const el = this.el(); el.style.opacity = '0'; setTimeout(() => { el.style.display = 'none'; el.style.visibility = 'hidden'; }, 200); // Wait for fade out animation // Clear any existing timeout if (this.showTimeout) { clearTimeout(this.showTimeout); this.showTimeout = null; } // Clean up mobile click handlers if they exist if (this.mobileClickHandler) { el.removeEventListener('click', this.mobileClickHandler); el.removeEventListener('touchend', this.mobileClickHandler); this.mobileClickHandler = null; } // Reset pointer events el.style.pointerEvents = 'none !important'; } /** * Clean up when component is disposed */ dispose() { if (this.showTimeout) { clearTimeout(this.showTimeout); } super.dispose(); } } // Register the component with Video.js videojs.registerComponent('SeekIndicator', SeekIndicator); export default SeekIndicator;