225 lines
7.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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 = `
<p style="margin: 0;">
<a href="${PUBLIC_URL}/media/${metadata.id}" target="_blank" style="color: #1890ff; text-decoration: none;">
View full player with reactions →
</a>
</p>
`;
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 = `
<strong style="color: #333;">${escapeHtml(metadata.title)}</strong>
${metadata.durationSeconds ? `<span style="margin-left: 8px;">• ${formatDuration(metadata.durationSeconds)}</span>` : ''}
${metadata.width && metadata.height ? `<span style="margin-left: 8px;">• ${metadata.width}×${metadata.height}</span>` : ''}
`;
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 = '<div class="video-loading" style="text-align: center; padding: 40px; color: #999;">Loading video...</div>';
// 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 = `
<div class="video-error" style="
padding: 40px;
text-align: center;
background: #fff3f3;
border: 1px solid #ffccc7;
border-radius: 8px;
color: #cf1322;
">
<svg style="width: 48px; height: 48px; margin-bottom: 12px;" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
</svg>
<p style="margin: 0; font-weight: 600;">${escapeHtml(message)}</p>
</div>
`;
}
/**
* 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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
/**
* 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);
});
}
})();