From 88e79d77800b8dcd6cfe8c7ced00347f7f720d65 Mon Sep 17 00:00:00 2001 From: overcuriousity Date: Wed, 13 Aug 2025 14:04:08 +0200 Subject: [PATCH] update video embed --- .env.example | 20 ---- src/components/Video.astro | 46 -------- src/styles/knowledgebase.css | 206 ++++++--------------------------- src/utils/remarkVideoPlugin.ts | 69 +++++++---- src/utils/videoUtils.ts | 115 ------------------ 5 files changed, 79 insertions(+), 377 deletions(-) delete mode 100644 src/components/Video.astro delete mode 100644 src/utils/videoUtils.ts diff --git a/.env.example b/.env.example index 26050b7..e0a1ee0 100644 --- a/.env.example +++ b/.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 # ============================================================================ diff --git a/src/components/Video.astro b/src/components/Video.astro deleted file mode 100644 index 4e16902..0000000 --- a/src/components/Video.astro +++ /dev/null @@ -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(':', '-')}`; ---- - -
- - {title !== 'Video' && ( - - )} -
\ No newline at end of file diff --git a/src/styles/knowledgebase.css b/src/styles/knowledgebase.css index 6537109..b3139f4 100644 --- a/src/styles/knowledgebase.css +++ b/src/styles/knowledgebase.css @@ -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; } } \ No newline at end of file diff --git a/src/utils/remarkVideoPlugin.ts b/src/utils/remarkVideoPlugin.ts index 5f0f596..11207f8 100644 --- a/src/utils/remarkVideoPlugin.ts +++ b/src/utils/remarkVideoPlugin.ts @@ -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, "'"); +} 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(' +
@@ -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(' + ${node.value} +
+ + `.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, "'"); -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/utils/videoUtils.ts b/src/utils/videoUtils.ts deleted file mode 100644 index bb4d9d7..0000000 --- a/src/utils/videoUtils.ts +++ /dev/null @@ -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 = { - 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, "'"); -} - -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' ? ` - - ` : ''; - - return ` -
- - ${metadataHTML} -
- `.trim(); -} \ No newline at end of file