diff options
Diffstat (limited to 'youtube/static/js/storyboard-preview.js')
| -rw-r--r-- | youtube/static/js/storyboard-preview.js | 375 |
1 files changed, 375 insertions, 0 deletions
diff --git a/youtube/static/js/storyboard-preview.js b/youtube/static/js/storyboard-preview.js new file mode 100644 index 0000000..8c09a69 --- /dev/null +++ b/youtube/static/js/storyboard-preview.js @@ -0,0 +1,375 @@ +/** + * YouTube Storyboard Preview Thumbnails + * Shows preview thumbnails when hovering over the progress bar + * Works with native HTML5 video player + * + * Fetches the proxied WebVTT storyboard from backend and extracts image URLs + */ +(function() { + 'use strict'; + + console.log('Storyboard Preview Thumbnails loaded'); + + // Storyboard configuration + let storyboardImages = []; // Array of {time, imageUrl, x, y, width, height} + let previewElement = null; + let tooltipElement = null; + let video = null; + let progressBarRect = null; + + /** + * Fetch and parse the storyboard VTT file + * The backend generates a VTT with proxied image URLs + */ + function fetchStoryboardVTT(vttUrl) { + return fetch(vttUrl) + .then(response => { + if (!response.ok) throw new Error('Failed to fetch storyboard VTT'); + return response.text(); + }) + .then(vttText => { + console.log('Fetched storyboard VTT, length:', vttText.length); + + const lines = vttText.split('\n'); + const images = []; + let currentEntry = null; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + + // Parse timestamp line: 00:00:00.000 --> 00:00:10.000 + if (line.includes('-->')) { + const timeMatch = line.match(/^(\d{2}):(\d{2}):(\d{2})\.(\d{3})/); + if (timeMatch) { + const hours = parseInt(timeMatch[1]); + const minutes = parseInt(timeMatch[2]); + const seconds = parseInt(timeMatch[3]); + const ms = parseInt(timeMatch[4]); + currentEntry = { + time: hours * 3600 + minutes * 60 + seconds + ms / 1000 + }; + } + } + // Parse image URL with crop parameters: /url#xywh=x,y,w,h + else if (line.includes('#xywh=') && currentEntry) { + const [urlPart, paramsPart] = line.split('#xywh='); + const [x, y, width, height] = paramsPart.split(',').map(Number); + + currentEntry.imageUrl = urlPart; + currentEntry.x = x; + currentEntry.y = y; + currentEntry.width = width; + currentEntry.height = height; + + images.push(currentEntry); + currentEntry = null; + } + } + + console.log('Parsed', images.length, 'storyboard frames'); + return images; + }); + } + + /** + * Format time as MM:SS or H:MM:SS + */ + function formatTime(seconds) { + if (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')}`; + } + return `${minutes}:${secs.toString().padStart(2, '0')}`; + } + + /** + * Find the closest storyboard frame for a given time + */ + function findFrameAtTime(time) { + if (!storyboardImages.length) return null; + + // Binary search for efficiency + let left = 0; + let right = storyboardImages.length - 1; + + while (left <= right) { + const mid = Math.floor((left + right) / 2); + const frame = storyboardImages[mid]; + + if (time >= frame.time && time < (storyboardImages[mid + 1]?.time || Infinity)) { + return frame; + } else if (time < frame.time) { + right = mid - 1; + } else { + left = mid + 1; + } + } + + // Return closest frame + return storyboardImages[Math.min(left, storyboardImages.length - 1)]; + } + + /** + * Detect browser + */ + function getBrowser() { + const ua = navigator.userAgent; + if (ua.indexOf('Firefox') > -1) return 'firefox'; + if (ua.indexOf('Chrome') > -1) return 'chrome'; + if (ua.indexOf('Safari') > -1) return 'safari'; + return 'other'; + } + + /** + * Detect the progress bar position in native video element + * Different browsers have different control layouts + */ + function detectProgressBar() { + if (!video) return null; + + const rect = video.getBoundingClientRect(); + const browser = getBrowser(); + + let progressBarArea; + + switch(browser) { + case 'firefox': + // Firefox: La barra de progreso está en la parte inferior pero más delgada + // Normalmente ocupa solo unos 20-25px de altura y está centrada + progressBarArea = { + top: rect.bottom - 30, // Área más pequeña para Firefox + bottom: rect.bottom - 5, // Dejamos espacio para otros controles + left: rect.left + 60, // Firefox tiene botones a la izquierda (play, volumen) + right: rect.right - 10, // Y a la derecha (fullscreen, etc) + height: 25 + }; + break; + + case 'chrome': + default: + // Chrome: La barra de progreso ocupa un área más grande + progressBarArea = { + top: rect.bottom - 50, + bottom: rect.bottom, + left: rect.left, + right: rect.right, + height: 50 + }; + break; + } + + return progressBarArea; + } + + /** + * Check if mouse is over the progress bar area + */ + function isOverProgressBar(mouseX, mouseY) { + if (!progressBarRect) return false; + + return mouseX >= progressBarRect.left && + mouseX <= progressBarRect.right && + mouseY >= progressBarRect.top && + mouseY <= progressBarRect.bottom; + } + + /** + * Initialize preview elements + */ + function initPreviewElements() { + video = document.getElementById('js-video-player'); + if (!video) { + console.error('Video element not found'); + return; + } + + console.log('Video element found, browser:', getBrowser()); + + // Create preview element + previewElement = document.createElement('div'); + previewElement.className = 'storyboard-preview'; + previewElement.style.cssText = ` + position: fixed; + display: none; + pointer-events: none; + z-index: 10000; + background: #000; + border: 2px solid #fff; + border-radius: 4px; + overflow: hidden; + box-shadow: 0 4px 12px rgba(0,0,0,0.5); + `; + + // Create tooltip element + tooltipElement = document.createElement('div'); + tooltipElement.className = 'storyboard-tooltip'; + tooltipElement.style.cssText = ` + position: absolute; + bottom: -25px; + left: 50%; + transform: translateX(-50%); + background: rgba(0,0,0,0.8); + color: #fff; + padding: 2px 6px; + border-radius: 3px; + font-size: 12px; + font-family: Arial, sans-serif; + white-space: nowrap; + pointer-events: none; + `; + + previewElement.appendChild(tooltipElement); + document.body.appendChild(previewElement); + + // Update progress bar position on mouse move + video.addEventListener('mousemove', updateProgressBarPosition); + } + + /** + * Update progress bar position detection + */ + function updateProgressBarPosition() { + progressBarRect = detectProgressBar(); + } + + /** + * Handle mouse move - only show preview when over progress bar area + */ + function handleMouseMove(e) { + if (!video || !storyboardImages.length) return; + + // Update progress bar position on each move + progressBarRect = detectProgressBar(); + + // Only show preview if mouse is over the progress bar area + if (!isOverProgressBar(e.clientX, e.clientY)) { + if (previewElement) previewElement.style.display = 'none'; + return; + } + + // Calculate position within the progress bar + const progressBarWidth = progressBarRect.right - progressBarRect.left; + let xInProgressBar = e.clientX - progressBarRect.left; + + // Adjust for Firefox's left offset + const browser = getBrowser(); + if (browser === 'firefox') { + // Ajustar el rango para que coincida mejor con la barra real + xInProgressBar = Math.max(0, Math.min(xInProgressBar, progressBarWidth)); + } + + const percentage = Math.max(0, Math.min(1, xInProgressBar / progressBarWidth)); + const time = percentage * video.duration; + const frame = findFrameAtTime(time); + + if (!frame) return; + + // Preview dimensions + const previewWidth = 160; + const previewHeight = 90; + const offsetFromCursor = 10; + + // Position above the cursor + let previewTop = e.clientY - previewHeight - offsetFromCursor; + + // If preview would go above the video, position below the cursor + const videoRect = video.getBoundingClientRect(); + if (previewTop < videoRect.top) { + previewTop = e.clientY + offsetFromCursor; + } + + // Keep preview within horizontal bounds + let left = e.clientX - (previewWidth / 2); + + // Ajustes específicos para Firefox + if (browser === 'firefox') { + // En Firefox, la barra no llega hasta los extremos + const minLeft = progressBarRect.left + 10; + const maxLeft = progressBarRect.right - previewWidth - 10; + left = Math.max(minLeft, Math.min(left, maxLeft)); + } else { + left = Math.max(videoRect.left, Math.min(left, videoRect.right - previewWidth)); + } + + // Apply all styles + previewElement.style.cssText = ` + display: block; + position: fixed; + left: ${left}px; + top: ${previewTop}px; + width: ${previewWidth}px; + height: ${previewHeight}px; + background-image: url('${frame.imageUrl}'); + background-position: -${frame.x}px -${frame.y}px; + background-size: auto; + background-repeat: no-repeat; + border: 2px solid #fff; + border-radius: 4px; + box-shadow: 0 4px 12px rgba(0,0,0,0.5); + z-index: 10000; + pointer-events: none; + `; + + tooltipElement.textContent = formatTime(time); + } + + /** + * Handle mouse leave video + */ + function handleMouseLeave() { + if (previewElement) { + previewElement.style.display = 'none'; + } + } + + /** + * Initialize storyboard preview + */ + function init() { + console.log('Initializing storyboard preview...'); + + // Check if storyboard URL is available + if (typeof storyboard_url === 'undefined' || !storyboard_url) { + console.log('No storyboard URL available'); + return; + } + + console.log('Storyboard URL:', storyboard_url); + + // Fetch the proxied VTT file from backend + fetchStoryboardVTT(storyboard_url) + .then(images => { + storyboardImages = images; + console.log('Loaded', images.length, 'storyboard images'); + + if (images.length === 0) { + console.log('No storyboard images parsed'); + return; + } + + initPreviewElements(); + + // Add event listeners to video + video.addEventListener('mousemove', handleMouseMove); + video.addEventListener('mouseleave', handleMouseLeave); + + console.log('Storyboard preview initialized for', getBrowser()); + }) + .catch(err => { + console.error('Failed to load storyboard:', err); + }); + } + + // Initialize when DOM is ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } + +})(); |
