/** * Video Player Hydration for MkDocs * * Automatically converts .video-block placeholders into actual video players * when landing pages with video blocks are exported to MkDocs. * * Supports both standard HTML5 video and advanced player with reactions. */ (function() { 'use strict'; // Configuration const MEDIA_API_URL = window.MEDIA_API_URL || 'http://localhost:4100'; const PUBLIC_URL = window.PUBLIC_URL || 'http://localhost:3000'; /** * Fetch video metadata from Media API */ async function fetchVideoMetadata(videoId) { try { const response = await fetch(`${MEDIA_API_URL}/api/videos/${videoId}/metadata`); if (!response.ok) { throw new Error(`Failed to fetch video ${videoId}: ${response.statusText}`); } return await response.json(); } catch (error) { console.error(`Error fetching video ${videoId} metadata:`, error); return null; } } /** * Create standard HTML5 video player */ function createStandardPlayer(metadata, options) { const video = document.createElement('video'); video.src = `${MEDIA_API_URL}/api/videos/${metadata.id}/stream`; video.poster = `${MEDIA_API_URL}/api/videos/${metadata.id}/thumbnail`; video.controls = options.controls !== false; video.autoplay = options.autoplay === true; video.loop = options.loop === true; video.muted = options.muted === true; video.style.width = options.width || '100%'; video.style.height = options.height || 'auto'; video.style.maxWidth = '100%'; video.style.borderRadius = '8px'; video.style.display = 'block'; // Add title attribute for accessibility video.setAttribute('title', metadata.title); video.setAttribute('aria-label', metadata.title); return video; } /** * Create advanced player container with reactions * Note: Full reaction functionality requires React components, * so this is a simplified version for MkDocs */ function createAdvancedPlayer(metadata, options) { const container = document.createElement('div'); container.className = 'advanced-video-container'; container.style.maxWidth = options.width || '100%'; container.style.margin = '0 auto'; // Video player const video = createStandardPlayer(metadata, options); container.appendChild(video); // Reaction placeholder (simplified for MkDocs) if (options.showReactions !== false) { const reactionBar = document.createElement('div'); reactionBar.className = 'video-reactions'; reactionBar.style.marginTop = '12px'; reactionBar.style.padding = '12px'; reactionBar.style.background = '#f5f5f5'; reactionBar.style.borderRadius = '8px'; reactionBar.style.textAlign = 'center'; reactionBar.style.fontSize = '14px'; reactionBar.style.color = '#666'; reactionBar.innerHTML = `

View full player with reactions →

`; container.appendChild(reactionBar); } // Video metadata const metaInfo = document.createElement('div'); metaInfo.className = 'video-meta'; metaInfo.style.marginTop = '8px'; metaInfo.style.fontSize = '13px'; metaInfo.style.color = '#999'; metaInfo.innerHTML = ` ${escapeHtml(metadata.title)} ${metadata.durationSeconds ? `• ${formatDuration(metadata.durationSeconds)}` : ''} ${metadata.width && metadata.height ? `• ${metadata.width}×${metadata.height}` : ''} `; container.appendChild(metaInfo); return container; } /** * Render a video block */ async function renderVideoBlock(blockElement) { const videoId = parseInt(blockElement.dataset.videoId, 10); const playerType = blockElement.dataset.playerType || 'standard'; const width = blockElement.dataset.width || '100%'; const height = blockElement.dataset.height || 'auto'; const autoplay = blockElement.dataset.autoplay === 'true'; const controls = blockElement.dataset.controls !== 'false'; const showReactions = blockElement.dataset.showReactions !== 'false'; if (!videoId || isNaN(videoId)) { console.warn('Invalid video ID in video block:', blockElement); showError(blockElement, 'Invalid video ID'); return; } // Show loading state blockElement.innerHTML = '
Loading video...
'; // Fetch metadata const metadata = await fetchVideoMetadata(videoId); if (!metadata) { showError(blockElement, `Video not found (ID: ${videoId})`); return; } // Create player const options = { width, height, autoplay, controls, showReactions }; const player = playerType === 'advanced' ? createAdvancedPlayer(metadata, options) : createStandardPlayer(metadata, options); // Replace placeholder with player blockElement.innerHTML = ''; blockElement.appendChild(player); console.log(`Rendered ${playerType} video player for video ${videoId}`); } /** * Show error state */ function showError(blockElement, message) { blockElement.innerHTML = `

${escapeHtml(message)}

`; } /** * Format duration in seconds to MM:SS */ function formatDuration(seconds) { const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${mins}:${secs.toString().padStart(2, '0')}`; } /** * Escape HTML to prevent XSS */ function escapeHtml(unsafe) { return String(unsafe) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } /** * Initialize all video blocks on page load */ function initVideoBlocks() { const blocks = document.querySelectorAll('.video-block'); console.log(`Found ${blocks.length} video block(s) to hydrate`); blocks.forEach(block => { // Skip if already rendered (has video element) if (block.querySelector('video')) { console.log('Video block already rendered, skipping'); return; } renderVideoBlock(block); }); } // Initialize when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initVideoBlocks); } else { initVideoBlocks(); } // Re-initialize on MkDocs navigation (for SPA-style navigation) if (typeof window.document$ !== 'undefined') { window.document$.subscribe(() => { console.log('MkDocs navigation detected, re-initializing video blocks'); setTimeout(initVideoBlocks, 100); }); } })();