update video embed
This commit is contained in:
		
							parent
							
								
									b630668897
								
							
						
					
					
						commit
						88e79d7780
					
				
							
								
								
									
										20
									
								
								.env.example
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								.env.example
									
									
									
									
									
								
							@ -68,26 +68,6 @@ AI_EMBEDDINGS_MODEL=mistral-embed
 | 
			
		||||
# User rate limiting (queries per minute)
 | 
			
		||||
AI_RATE_LIMIT_MAX_REQUESTS=4
 | 
			
		||||
 | 
			
		||||
# ============================================================================
 | 
			
		||||
# 🎥 VIDEO EMBEDDING - PRODUCTION CONFIGURATION
 | 
			
		||||
# ============================================================================
 | 
			
		||||
 | 
			
		||||
# Enable local caching of Nextcloud videos (highly recommended)
 | 
			
		||||
VIDEO_CACHE_ENABLED=true
 | 
			
		||||
 | 
			
		||||
# Directory for cached videos (ensure it's writable and has sufficient space)
 | 
			
		||||
# This directory will grow over time as videos are cached permanently
 | 
			
		||||
VIDEO_CACHE_DIR=./cache/videos
 | 
			
		||||
 | 
			
		||||
# Emergency cleanup threshold in MB - videos are cached indefinitely
 | 
			
		||||
# Only triggers cleanup when approaching this limit to prevent disk full
 | 
			
		||||
# Recommended: 2000MB (2GB) for small deployments, 5000MB+ for larger ones
 | 
			
		||||
VIDEO_CACHE_MAX_SIZE=2000
 | 
			
		||||
 | 
			
		||||
# Maximum individual video file size for caching in MB
 | 
			
		||||
# Videos larger than this will stream directly without caching
 | 
			
		||||
VIDEO_MAX_SIZE=200
 | 
			
		||||
 | 
			
		||||
# ============================================================================
 | 
			
		||||
# CACHING BEHAVIOR
 | 
			
		||||
# ============================================================================
 | 
			
		||||
 | 
			
		||||
@ -1,46 +0,0 @@
 | 
			
		||||
---
 | 
			
		||||
// src/components/Video.astro - SIMPLE responsive video component
 | 
			
		||||
export interface Props {
 | 
			
		||||
  src: string;
 | 
			
		||||
  title?: string;
 | 
			
		||||
  controls?: boolean;
 | 
			
		||||
  autoplay?: boolean;
 | 
			
		||||
  muted?: boolean;
 | 
			
		||||
  loop?: boolean;
 | 
			
		||||
  aspectRatio?: '16:9' | '4:3' | '1:1';
 | 
			
		||||
  preload?: 'none' | 'metadata' | 'auto';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const {
 | 
			
		||||
  src,
 | 
			
		||||
  title = 'Video',
 | 
			
		||||
  controls = true,
 | 
			
		||||
  autoplay = false,
 | 
			
		||||
  muted = false,
 | 
			
		||||
  loop = false,
 | 
			
		||||
  aspectRatio = '16:9',
 | 
			
		||||
  preload = 'metadata'
 | 
			
		||||
} = Astro.props;
 | 
			
		||||
 | 
			
		||||
const aspectClass = `aspect-${aspectRatio.replace(':', '-')}`;
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
<div class={`video-container ${aspectClass}`}>
 | 
			
		||||
  <video 
 | 
			
		||||
    src={src}
 | 
			
		||||
    controls={controls}
 | 
			
		||||
    autoplay={autoplay}
 | 
			
		||||
    muted={muted}
 | 
			
		||||
    loop={loop}
 | 
			
		||||
    preload={preload}
 | 
			
		||||
    style="width: 100%; height: 100%;"
 | 
			
		||||
    data-video-title={title}
 | 
			
		||||
  >
 | 
			
		||||
    <p>Your browser does not support the video element.</p>
 | 
			
		||||
  </video>
 | 
			
		||||
  {title !== 'Video' && (
 | 
			
		||||
    <div class="video-metadata">
 | 
			
		||||
      <div class="video-title">{title}</div>
 | 
			
		||||
    </div>
 | 
			
		||||
  )}
 | 
			
		||||
</div>
 | 
			
		||||
@ -691,12 +691,11 @@
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/* ==========================================================================
 | 
			
		||||
   VIDEO EMBEDDING - Add to knowledgebase.css
 | 
			
		||||
   VIDEO EMBEDDING - ULTRA SIMPLE: Just full width, natural aspect ratios
 | 
			
		||||
   ========================================================================== */
 | 
			
		||||
 | 
			
		||||
/* Video Container and Responsive Wrapper */
 | 
			
		||||
/* Video Container - just a styled wrapper */
 | 
			
		||||
:where(.markdown-content) .video-container {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  margin: 2rem 0;
 | 
			
		||||
  border-radius: var(--radius-lg, 0.75rem);
 | 
			
		||||
@ -705,84 +704,34 @@
 | 
			
		||||
  box-shadow: var(--shadow-lg, 0 12px 30px rgba(0,0,0,0.16));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Responsive 16:9 aspect ratio by default */
 | 
			
		||||
:where(.markdown-content) .video-container.aspect-16-9 {
 | 
			
		||||
  aspect-ratio: 16 / 9;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:where(.markdown-content) .video-container.aspect-4-3 {
 | 
			
		||||
  aspect-ratio: 4 / 3;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:where(.markdown-content) .video-container.aspect-1-1 {
 | 
			
		||||
  aspect-ratio: 1 / 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Video Element Styling */
 | 
			
		||||
/* Video Element - full width, natural aspect ratio */
 | 
			
		||||
:where(.markdown-content) .video-container video {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  object-fit: contain;
 | 
			
		||||
  height: auto;
 | 
			
		||||
  display: block;
 | 
			
		||||
  background-color: #000;
 | 
			
		||||
  border: none;
 | 
			
		||||
  outline: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Custom Video Controls Enhancement */
 | 
			
		||||
:where(.markdown-content) video::-webkit-media-controls-panel {
 | 
			
		||||
  background-color: rgba(0, 0, 0, 0.8);
 | 
			
		||||
/* YouTube iframe - full width, preserve embedded dimensions ratio */
 | 
			
		||||
:where(.markdown-content) .video-container iframe {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: auto;
 | 
			
		||||
  aspect-ratio: 16 / 9; /* Only for iframes since they don't have intrinsic ratio */
 | 
			
		||||
  display: block;
 | 
			
		||||
  border: none;
 | 
			
		||||
  outline: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:where(.markdown-content) video::-webkit-media-controls-current-time-display,
 | 
			
		||||
:where(.markdown-content) video::-webkit-media-controls-time-remaining-display {
 | 
			
		||||
  color: white;
 | 
			
		||||
  text-shadow: none;
 | 
			
		||||
/* Focus states for accessibility */
 | 
			
		||||
:where(.markdown-content) .video-container video:focus,
 | 
			
		||||
:where(.markdown-content) .video-container iframe:focus {
 | 
			
		||||
  outline: 3px solid var(--color-primary);
 | 
			
		||||
  outline-offset: 3px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Video Loading State */
 | 
			
		||||
:where(.markdown-content) .video-container .video-loading {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  top: 50%;
 | 
			
		||||
  left: 50%;
 | 
			
		||||
  transform: translate(-50%, -50%);
 | 
			
		||||
  color: var(--color-text-secondary);
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  gap: 1rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:where(.markdown-content) .video-container .video-loading .spinner {
 | 
			
		||||
  width: 2rem;
 | 
			
		||||
  height: 2rem;
 | 
			
		||||
  border: 3px solid var(--color-border);
 | 
			
		||||
  border-top: 3px solid var(--color-primary);
 | 
			
		||||
  border-radius: 50%;
 | 
			
		||||
  animation: spin 1s linear infinite;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes spin {
 | 
			
		||||
  0% { transform: rotate(0deg); }
 | 
			
		||||
  100% { transform: rotate(360deg); }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Video Error State */
 | 
			
		||||
:where(.markdown-content) .video-container .video-error {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  top: 50%;
 | 
			
		||||
  left: 50%;
 | 
			
		||||
  transform: translate(-50%, -50%);
 | 
			
		||||
  text-align: center;
 | 
			
		||||
  color: var(--color-error, #dc3545);
 | 
			
		||||
  padding: 2rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:where(.markdown-content) .video-container .video-error .error-icon {
 | 
			
		||||
  font-size: 3rem;
 | 
			
		||||
  margin-bottom: 1rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Video Metadata Overlay */
 | 
			
		||||
/* Video Metadata */
 | 
			
		||||
:where(.markdown-content) .video-metadata {
 | 
			
		||||
  background-color: var(--color-bg-secondary);
 | 
			
		||||
  border: 1px solid var(--color-border);
 | 
			
		||||
@ -796,69 +745,13 @@
 | 
			
		||||
:where(.markdown-content) .video-metadata .video-title {
 | 
			
		||||
  font-weight: 600;
 | 
			
		||||
  color: var(--color-text);
 | 
			
		||||
  margin-bottom: 0.5rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:where(.markdown-content) .video-metadata .video-info {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  gap: 1rem;
 | 
			
		||||
  flex-wrap: wrap;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:where(.markdown-content) .video-metadata .video-duration,
 | 
			
		||||
:where(.markdown-content) .video-metadata .video-size,
 | 
			
		||||
:where(.markdown-content) .video-metadata .video-format {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  gap: 0.25rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Fullscreen Support */
 | 
			
		||||
:where(.markdown-content) .video-container video:fullscreen {
 | 
			
		||||
  background-color: #000;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:where(.markdown-content) .video-container video:-webkit-full-screen {
 | 
			
		||||
  background-color: #000;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:where(.markdown-content) .video-container video:-moz-full-screen {
 | 
			
		||||
  background-color: #000;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Video Thumbnail/Poster Styling */
 | 
			
		||||
:where(.markdown-content) .video-container video[poster] {
 | 
			
		||||
  object-fit: cover;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Protected Video Overlay */
 | 
			
		||||
:where(.markdown-content) .video-container .video-protected {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  top: 0;
 | 
			
		||||
  left: 0;
 | 
			
		||||
  right: 0;
 | 
			
		||||
  bottom: 0;
 | 
			
		||||
  background-color: rgba(0, 0, 0, 0.8);
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  color: white;
 | 
			
		||||
  text-align: center;
 | 
			
		||||
  padding: 2rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:where(.markdown-content) .video-container .video-protected .lock-icon {
 | 
			
		||||
  font-size: 3rem;
 | 
			
		||||
  margin-bottom: 1rem;
 | 
			
		||||
  opacity: 0.8;
 | 
			
		||||
  margin: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Responsive Design */
 | 
			
		||||
@media (max-width: 768px) {
 | 
			
		||||
  :where(.markdown-content) .video-container {
 | 
			
		||||
    margin: 1.5rem -0.5rem; /* Extend to edges on mobile */
 | 
			
		||||
    margin: 1.5rem -0.5rem;
 | 
			
		||||
    border-radius: 0;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
@ -867,15 +760,9 @@
 | 
			
		||||
    font-size: 0.8125rem;
 | 
			
		||||
    border-radius: 0;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  :where(.markdown-content) .video-metadata .video-info {
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    gap: 0.5rem;
 | 
			
		||||
    align-items: flex-start;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Dark Theme Adjustments */
 | 
			
		||||
/* Dark Theme */
 | 
			
		||||
[data-theme="dark"] :where(.markdown-content) .video-container {
 | 
			
		||||
  box-shadow: 0 12px 30px rgba(0,0,0,0.4);
 | 
			
		||||
}
 | 
			
		||||
@ -885,48 +772,23 @@
 | 
			
		||||
  border-color: color-mix(in srgb, var(--color-border) 60%, transparent);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Video Caption/Description Support */
 | 
			
		||||
:where(.markdown-content) .video-caption {
 | 
			
		||||
  margin-top: 1rem;
 | 
			
		||||
  font-size: 0.9375rem;
 | 
			
		||||
  color: var(--color-text-secondary);
 | 
			
		||||
  text-align: center;
 | 
			
		||||
  font-style: italic;
 | 
			
		||||
  line-height: 1.5;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Video Gallery Support (multiple videos) */
 | 
			
		||||
:where(.markdown-content) .video-gallery {
 | 
			
		||||
  display: grid;
 | 
			
		||||
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
 | 
			
		||||
  gap: 2rem;
 | 
			
		||||
  margin: 2rem 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:where(.markdown-content) .video-gallery .video-container {
 | 
			
		||||
  margin: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Accessibility Improvements */
 | 
			
		||||
:where(.markdown-content) .video-container video:focus {
 | 
			
		||||
  outline: 3px solid var(--color-primary);
 | 
			
		||||
  outline-offset: 3px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Print Media - Hide Videos */
 | 
			
		||||
/* Print Media */
 | 
			
		||||
@media print {
 | 
			
		||||
  :where(.markdown-content) .video-container {
 | 
			
		||||
    border: 2px solid #ddd;
 | 
			
		||||
    background-color: #f5f5f5;
 | 
			
		||||
    padding: 2rem;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  :where(.markdown-content) .video-container video,
 | 
			
		||||
  :where(.markdown-content) .video-container iframe {
 | 
			
		||||
    display: none !important;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  :where(.markdown-content) .video-container::after {
 | 
			
		||||
    content: "[Video: " attr(data-video-title, "Embedded Video") "]";
 | 
			
		||||
  :where(.markdown-content) .video-container::before {
 | 
			
		||||
    content: "📹 Video: " attr(data-video-title, "Embedded Video");
 | 
			
		||||
    display: block;
 | 
			
		||||
    padding: 1rem;
 | 
			
		||||
    background-color: #f5f5f5;
 | 
			
		||||
    border: 1px solid #ddd;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
    font-style: italic;
 | 
			
		||||
    color: #666;
 | 
			
		||||
    font-weight: 600;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -1,39 +1,49 @@
 | 
			
		||||
// src/utils/remarkVideoPlugin.ts
 | 
			
		||||
// src/utils/remarkVideoPlugin.ts - SIMPLIFIED: Basic video and iframe enhancement only
 | 
			
		||||
import { visit } from 'unist-util-visit';
 | 
			
		||||
import type { Plugin } from 'unified';
 | 
			
		||||
import type { Root } from 'hast';
 | 
			
		||||
 | 
			
		||||
function escapeHtml(unsafe: string): string {
 | 
			
		||||
  if (typeof unsafe !== 'string') return '';
 | 
			
		||||
  
 | 
			
		||||
  return unsafe
 | 
			
		||||
    .replace(/&/g, "&")
 | 
			
		||||
    .replace(/</g, "<")
 | 
			
		||||
    .replace(/>/g, ">")
 | 
			
		||||
    .replace(/"/g, """)
 | 
			
		||||
    .replace(/'/g, "'");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const remarkVideoPlugin: Plugin<[], Root> = () => {
 | 
			
		||||
  return (tree: Root) => {
 | 
			
		||||
    // Process video tags
 | 
			
		||||
    visit(tree, 'html', (node: any, index: number | undefined, parent: any) => {
 | 
			
		||||
      if (node.value && node.value.includes('<video') && typeof index === 'number') {
 | 
			
		||||
        
 | 
			
		||||
        const srcMatch = node.value.match(/src=["']([^"']+)["']/);
 | 
			
		||||
        const titleMatch = node.value.match(/title=["']([^"']+)["']/);
 | 
			
		||||
        
 | 
			
		||||
        if (srcMatch) {
 | 
			
		||||
          const originalSrc = srcMatch[1];
 | 
			
		||||
          const title = titleMatch?.[1] || 'Video';
 | 
			
		||||
                    
 | 
			
		||||
          
 | 
			
		||||
          // Extract existing attributes
 | 
			
		||||
          const hasControls = node.value.includes('controls');
 | 
			
		||||
          const hasAutoplay = node.value.includes('autoplay');
 | 
			
		||||
          const hasMuted = node.value.includes('muted');
 | 
			
		||||
          const hasLoop = node.value.includes('loop');
 | 
			
		||||
          const hasPreload = node.value.match(/preload=["']([^"']+)["']/);
 | 
			
		||||
          const preloadMatch = node.value.match(/preload=["']([^"']+)["']/);
 | 
			
		||||
          
 | 
			
		||||
          // Create enhanced video with responsive wrapper
 | 
			
		||||
          const enhancedHTML = `
 | 
			
		||||
            <div class="video-container aspect-16-9">
 | 
			
		||||
            <div class="video-container">
 | 
			
		||||
              <video 
 | 
			
		||||
                src="${escapeHtml(originalSrc)}"
 | 
			
		||||
                ${hasControls ? 'controls' : ''}
 | 
			
		||||
                ${hasAutoplay ? 'autoplay' : ''}
 | 
			
		||||
                ${hasMuted ? 'muted' : ''}
 | 
			
		||||
                ${hasLoop ? 'loop' : ''}
 | 
			
		||||
                ${hasPreload ? `preload="${hasPreload[1]}"` : 'preload="metadata"'}
 | 
			
		||||
                style="width: 100%; height: 100%;"
 | 
			
		||||
                ${preloadMatch ? `preload="${preloadMatch[1]}"` : 'preload="metadata"'}
 | 
			
		||||
                data-video-title="${escapeHtml(title)}"
 | 
			
		||||
                data-original-src="${escapeHtml(originalSrc)}"
 | 
			
		||||
              >
 | 
			
		||||
                <p>Your browser does not support the video element.</p>
 | 
			
		||||
              </video>
 | 
			
		||||
@ -46,23 +56,34 @@ export const remarkVideoPlugin: Plugin<[], Root> = () => {
 | 
			
		||||
          `.trim();
 | 
			
		||||
          
 | 
			
		||||
          parent.children[index] = { type: 'html', value: enhancedHTML };
 | 
			
		||||
          
 | 
			
		||||
          console.log(`[VIDEO] Processed: ${title}`);
 | 
			
		||||
          console.log(`[VIDEO] Final URL: ${originalSrc}`);
 | 
			
		||||
          console.log(`[VIDEO] Enhanced: ${title} (${originalSrc})`);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      // Process all iframes with consistent styling
 | 
			
		||||
      if (node.value && node.value.includes('<iframe') && typeof index === 'number' && parent) {
 | 
			
		||||
        
 | 
			
		||||
        // Skip if already wrapped in video-container to prevent double-wrapping
 | 
			
		||||
        if (node.value.includes('video-container')) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        const titleMatch = node.value.match(/title=["']([^"']+)["']/);
 | 
			
		||||
        const title = titleMatch?.[1] || 'Embedded Video';
 | 
			
		||||
        
 | 
			
		||||
        // Wrap iframe in simple responsive container
 | 
			
		||||
        const enhancedHTML = `
 | 
			
		||||
          <div class="video-container">
 | 
			
		||||
            ${node.value}
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="video-metadata">
 | 
			
		||||
            <div class="video-title">${escapeHtml(title)}</div>
 | 
			
		||||
          </div>
 | 
			
		||||
        `.trim();
 | 
			
		||||
        
 | 
			
		||||
        parent.children[index] = { type: 'html', value: enhancedHTML };
 | 
			
		||||
        console.log(`[VIDEO] Enhanced iframe: ${title}`);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
function escapeHtml(unsafe: string): string {
 | 
			
		||||
  if (typeof unsafe !== 'string') return '';
 | 
			
		||||
  
 | 
			
		||||
  return unsafe
 | 
			
		||||
    .replace(/&/g, "&")
 | 
			
		||||
    .replace(/</g, "<")
 | 
			
		||||
    .replace(/>/g, ">")
 | 
			
		||||
    .replace(/"/g, """)
 | 
			
		||||
    .replace(/'/g, "'");
 | 
			
		||||
}
 | 
			
		||||
};
 | 
			
		||||
@ -1,115 +0,0 @@
 | 
			
		||||
// src/utils/videoUtils.ts - SIMPLIFIED - Basic utilities only
 | 
			
		||||
import 'dotenv/config';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export interface SimpleVideoMetadata {
 | 
			
		||||
  title?: string;
 | 
			
		||||
  description?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getVideoMimeType(url: string): string {
 | 
			
		||||
  let extension: string | undefined;
 | 
			
		||||
  try {
 | 
			
		||||
    const pathname = new URL(url).pathname;
 | 
			
		||||
    extension = pathname.split('.').pop()?.toLowerCase();
 | 
			
		||||
  } catch {
 | 
			
		||||
    extension = url.split('?')[0].split('.').pop()?.toLowerCase();
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  const mimeTypes: Record<string, string> = {
 | 
			
		||||
    mp4: 'video/mp4',
 | 
			
		||||
    webm: 'video/webm',
 | 
			
		||||
    ogg: 'video/ogg',
 | 
			
		||||
    mov: 'video/quicktime',
 | 
			
		||||
    avi: 'video/x-msvideo',
 | 
			
		||||
    m4v: 'video/m4v',
 | 
			
		||||
    mkv: 'video/x-matroska',
 | 
			
		||||
    flv: 'video/x-flv'
 | 
			
		||||
  };
 | 
			
		||||
  
 | 
			
		||||
  return (extension && mimeTypes[extension]) || 'video/mp4';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function formatDuration(seconds: number): string {
 | 
			
		||||
  const hours = Math.floor(seconds / 3600);
 | 
			
		||||
  const minutes = Math.floor((seconds % 3600) / 60);
 | 
			
		||||
  const remainingSeconds = Math.floor(seconds % 60);
 | 
			
		||||
  
 | 
			
		||||
  if (hours > 0) {
 | 
			
		||||
    return `${hours}:${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function formatFileSize(bytes: number): string {
 | 
			
		||||
  if (bytes < 1024) return `${bytes} B`;
 | 
			
		||||
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
 | 
			
		||||
  if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
 | 
			
		||||
  return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function escapeHtml(unsafe: string): string {
 | 
			
		||||
  if (typeof unsafe !== 'string') return '';
 | 
			
		||||
  
 | 
			
		||||
  return unsafe
 | 
			
		||||
    .replace(/&/g, "&")
 | 
			
		||||
    .replace(/</g, "<")
 | 
			
		||||
    .replace(/>/g, ">")
 | 
			
		||||
    .replace(/"/g, """)
 | 
			
		||||
    .replace(/'/g, "'");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function generateVideoHTML(
 | 
			
		||||
  src: string, 
 | 
			
		||||
  options: {
 | 
			
		||||
    title?: string;
 | 
			
		||||
    controls?: boolean;
 | 
			
		||||
    autoplay?: boolean;
 | 
			
		||||
    muted?: boolean;
 | 
			
		||||
    loop?: boolean;
 | 
			
		||||
    preload?: 'none' | 'metadata' | 'auto';
 | 
			
		||||
    aspectRatio?: '16:9' | '4:3' | '1:1';
 | 
			
		||||
    showMetadata?: boolean;
 | 
			
		||||
  } = {}
 | 
			
		||||
): string {
 | 
			
		||||
  const {
 | 
			
		||||
    title = 'Video',
 | 
			
		||||
    controls = true,
 | 
			
		||||
    autoplay = false,
 | 
			
		||||
    muted = false,
 | 
			
		||||
    loop = false,
 | 
			
		||||
    preload = 'metadata',
 | 
			
		||||
    aspectRatio = '16:9',
 | 
			
		||||
    showMetadata = true
 | 
			
		||||
  } = options;
 | 
			
		||||
  
 | 
			
		||||
  const aspectClass = `aspect-${aspectRatio.replace(':', '-')}`;
 | 
			
		||||
  const videoAttributes = [
 | 
			
		||||
    controls ? 'controls' : '',
 | 
			
		||||
    autoplay ? 'autoplay' : '',
 | 
			
		||||
    muted ? 'muted' : '',
 | 
			
		||||
    loop ? 'loop' : '',
 | 
			
		||||
    `preload="${preload}"`
 | 
			
		||||
  ].filter(Boolean).join(' ');
 | 
			
		||||
  
 | 
			
		||||
  const metadataHTML = showMetadata && title !== 'Video' ? `
 | 
			
		||||
    <div class="video-metadata">
 | 
			
		||||
      <div class="video-title">${escapeHtml(title)}</div>
 | 
			
		||||
    </div>
 | 
			
		||||
  ` : '';
 | 
			
		||||
  
 | 
			
		||||
  return `
 | 
			
		||||
    <div class="video-container ${aspectClass}">
 | 
			
		||||
      <video 
 | 
			
		||||
        src="${escapeHtml(src)}"
 | 
			
		||||
        ${videoAttributes}
 | 
			
		||||
        style="width: 100%; height: 100%;"
 | 
			
		||||
        data-video-title="${escapeHtml(title)}"
 | 
			
		||||
      >
 | 
			
		||||
        <p>Your browser does not support the video element.</p>
 | 
			
		||||
      </video>
 | 
			
		||||
      ${metadataHTML}
 | 
			
		||||
    </div>
 | 
			
		||||
  `.trim();
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user