videos
This commit is contained in:
		
							parent
							
								
									f159f904f0
								
							
						
					
					
						commit
						2d920391ad
					
				@ -18,6 +18,9 @@ sections:
 | 
				
			|||||||
review_status: "published"
 | 
					review_status: "published"
 | 
				
			||||||
---
 | 
					---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<video src="https://console.s3.cc24.dev/browser/forensic-pathways/20250610_AllgemeineForensikII_Vorlesung.mp4" controls title="MinIO Video Demo"></video>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
> **⚠️ Hinweis**: Dies ist ein vorläufiger, KI-generierter Knowledgebase-Eintrag. Wir freuen uns über Verbesserungen und Ergänzungen durch die Community!
 | 
					> **⚠️ Hinweis**: Dies ist ein vorläufiger, KI-generierter Knowledgebase-Eintrag. Wir freuen uns über Verbesserungen und Ergänzungen durch die Community!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -24,6 +24,7 @@ interface VideoConfig {
 | 
				
			|||||||
 * 3. HTML video tags: <video src="url"></video>
 | 
					 * 3. HTML video tags: <video src="url"></video>
 | 
				
			||||||
 * 4. Link syntax with video: [Video Title](url.mp4)
 | 
					 * 4. Link syntax with video: [Video Title](url.mp4)
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					// REPLACE the transformer body to collect async tasks and call the processor
 | 
				
			||||||
export const remarkVideoPlugin: Plugin<[VideoConfig?], Root> = (config = {}) => {
 | 
					export const remarkVideoPlugin: Plugin<[VideoConfig?], Root> = (config = {}) => {
 | 
				
			||||||
  const {
 | 
					  const {
 | 
				
			||||||
    enableAsync = true,
 | 
					    enableAsync = true,
 | 
				
			||||||
@ -37,84 +38,75 @@ export const remarkVideoPlugin: Plugin<[VideoConfig?], Root> = (config = {}) =>
 | 
				
			|||||||
  } = config;
 | 
					  } = config;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return async (tree: Root) => {
 | 
					  return async (tree: Root) => {
 | 
				
			||||||
    const videoNodes: Array<{ node: any; parent: any; index: number; replacement: any }> = [];
 | 
					    const tasks: Array<Promise<void>> = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Find video directives (:::video{...})
 | 
					    // :::video{...}
 | 
				
			||||||
    visit(tree, 'textDirective', (node: any, index: number | undefined, parent: any) => {
 | 
					    visit(tree, 'textDirective', (node: any, index: number | undefined, parent: any) => {
 | 
				
			||||||
      if (node.name === 'video' && typeof index === 'number') {
 | 
					      if (node.name === 'video' && typeof index === 'number') {
 | 
				
			||||||
        const videoNode = processVideoDirective(node, defaultOptions);
 | 
					        tasks.push((async () => {
 | 
				
			||||||
        if (videoNode) {
 | 
					          const replacement = await processVideoDirectiveAsync(node, defaultOptions);
 | 
				
			||||||
          videoNodes.push({ node, parent, index, replacement: videoNode });
 | 
					          if (replacement && parent?.children) parent.children[index] = replacement;
 | 
				
			||||||
        }
 | 
					        })());
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Find container directives (:::video ... :::)
 | 
					    // :::video ... :::
 | 
				
			||||||
    visit(tree, 'containerDirective', (node: any, index: number | undefined, parent: any) => {
 | 
					    visit(tree, 'containerDirective', (node: any, index: number | undefined, parent: any) => {
 | 
				
			||||||
      if (node.name === 'video' && typeof index === 'number') {
 | 
					      if (node.name === 'video' && typeof index === 'number') {
 | 
				
			||||||
        const videoNode = processVideoDirective(node, defaultOptions);
 | 
					        tasks.push((async () => {
 | 
				
			||||||
        if (videoNode) {
 | 
					          const replacement = await processVideoDirectiveAsync(node, defaultOptions);
 | 
				
			||||||
          videoNodes.push({ node, parent, index, replacement: videoNode });
 | 
					          if (replacement && parent?.children) parent.children[index] = replacement;
 | 
				
			||||||
        }
 | 
					        })());
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Find image nodes that might be videos
 | 
					    // 
 | 
				
			||||||
    visit(tree, 'image', (node: any, index: number | undefined, parent: any) => {
 | 
					    visit(tree, 'image', (node: any, index: number | undefined, parent: any) => {
 | 
				
			||||||
      if (isVideoUrl(node.url) && typeof index === 'number') {
 | 
					      if (isVideoUrl(node.url) && typeof index === 'number') {
 | 
				
			||||||
        const videoNode = processImageAsVideo(node, defaultOptions);
 | 
					        tasks.push((async () => {
 | 
				
			||||||
        if (videoNode) {
 | 
					          const replacement = await processImageAsVideoAsync(node, defaultOptions);
 | 
				
			||||||
          videoNodes.push({ node, parent, index, replacement: videoNode });
 | 
					          if (replacement && parent?.children) parent.children[index] = replacement;
 | 
				
			||||||
        }
 | 
					        })());
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Find link nodes that point to videos
 | 
					    // [Title](video.mp4)
 | 
				
			||||||
    visit(tree, 'link', (node: any, index: number | undefined, parent: any) => {
 | 
					    visit(tree, 'link', (node: any, index: number | undefined, parent: any) => {
 | 
				
			||||||
      if (isVideoUrl(node.url) && typeof index === 'number') {
 | 
					      if (isVideoUrl(node.url) && typeof index === 'number') {
 | 
				
			||||||
        const videoNode = processLinkAsVideo(node, defaultOptions);
 | 
					        tasks.push((async () => {
 | 
				
			||||||
        if (videoNode) {
 | 
					          const replacement = await processLinkAsVideoAsync(node, defaultOptions);
 | 
				
			||||||
          videoNodes.push({ node, parent, index, replacement: videoNode });
 | 
					          if (replacement && parent?.children) parent.children[index] = replacement;
 | 
				
			||||||
        }
 | 
					        })());
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Process HTML video tags in the tree
 | 
					    // Raw <video ...>
 | 
				
			||||||
    visit(tree, 'html', (node: any, index: number | undefined, parent: any) => {
 | 
					    visit(tree, 'html', (node: any, index: number | undefined, parent: any) => {
 | 
				
			||||||
      if (node.value && node.value.includes('<video') && typeof index === 'number') {
 | 
					      if (node.value && node.value.includes('<video') && typeof index === 'number') {
 | 
				
			||||||
        const videoNode = processHTMLVideo(node, defaultOptions);
 | 
					        tasks.push((async () => {
 | 
				
			||||||
        if (videoNode) {
 | 
					          const replacement = await processHTMLVideoAsync(node, defaultOptions);
 | 
				
			||||||
          videoNodes.push({ node, parent, index, replacement: videoNode });
 | 
					          if (replacement && parent?.children) parent.children[index] = replacement;
 | 
				
			||||||
        }
 | 
					        })());
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Replace all found video nodes
 | 
					    await Promise.all(tasks);
 | 
				
			||||||
    for (const { parent, index, replacement } of videoNodes.reverse()) {
 | 
					 | 
				
			||||||
      if (parent && parent.children && Array.isArray(parent.children)) {
 | 
					 | 
				
			||||||
        parent.children[index] = replacement;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function processVideoDirective(node: any, defaultOptions: VideoConfig['defaultOptions']): any | null {
 | 
					
 | 
				
			||||||
 | 
					async function processVideoDirectiveAsync(node: any, defaultOptions: VideoConfig['defaultOptions']): Promise<any | null> {
 | 
				
			||||||
  const attributes = node.attributes || {};
 | 
					  const attributes = node.attributes || {};
 | 
				
			||||||
  const src = attributes.src;
 | 
					  const src = attributes.src;
 | 
				
			||||||
  
 | 
					 | 
				
			||||||
  if (!src) {
 | 
					  if (!src) {
 | 
				
			||||||
    console.warn('[VIDEO PLUGIN] Video directive missing src attribute');
 | 
					    console.warn('[VIDEO PLUGIN] Video directive missing src attribute');
 | 
				
			||||||
    return null;
 | 
					    return null;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Validate and normalize aspect ratio
 | 
					 | 
				
			||||||
  const aspectRatioValue = attributes.aspectRatio || attributes['aspect-ratio'] || defaultOptions?.aspectRatio;
 | 
					  const aspectRatioValue = attributes.aspectRatio || attributes['aspect-ratio'] || defaultOptions?.aspectRatio;
 | 
				
			||||||
  const validAspectRatios = ['16:9', '4:3', '1:1'] as const;
 | 
					  const validAspectRatios = ['16:9', '4:3', '1:1'] as const;
 | 
				
			||||||
  const aspectRatio = validAspectRatios.includes(aspectRatioValue as any) 
 | 
					  const aspectRatio = (validAspectRatios.includes(aspectRatioValue as any) ? aspectRatioValue : '16:9') as '16:9' | '4:3' | '1:1';
 | 
				
			||||||
    ? aspectRatioValue as '16:9' | '4:3' | '1:1'
 | 
					 | 
				
			||||||
    : '16:9';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Extract options from attributes
 | 
					 | 
				
			||||||
  const options = {
 | 
					  const options = {
 | 
				
			||||||
    controls: attributes.controls !== undefined ? attributes.controls !== 'false' : defaultOptions?.controls ?? true,
 | 
					    controls: attributes.controls !== undefined ? attributes.controls !== 'false' : defaultOptions?.controls ?? true,
 | 
				
			||||||
    autoplay: attributes.autoplay !== undefined ? attributes.autoplay !== 'false' : defaultOptions?.autoplay ?? false,
 | 
					    autoplay: attributes.autoplay !== undefined ? attributes.autoplay !== 'false' : defaultOptions?.autoplay ?? false,
 | 
				
			||||||
@ -131,87 +123,68 @@ function processVideoDirective(node: any, defaultOptions: VideoConfig['defaultOp
 | 
				
			|||||||
    poster: attributes.poster
 | 
					    poster: attributes.poster
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return createVideoElement(src, metadata, options);
 | 
					  const html = await buildProcessedVideoHTML(src, metadata, options);
 | 
				
			||||||
 | 
					  return { type: 'html', value: html };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function processImageAsVideo(node: any, defaultOptions: VideoConfig['defaultOptions']): any | null {
 | 
					async function processImageAsVideoAsync(node: any, defaultOptions: VideoConfig['defaultOptions']): Promise<any | null> {
 | 
				
			||||||
  const src = node.url;
 | 
					  const src = node.url;
 | 
				
			||||||
  const metadata = {
 | 
					  const metadata = { title: node.title || node.alt, description: node.alt };
 | 
				
			||||||
    title: node.title || node.alt,
 | 
					 | 
				
			||||||
    description: node.alt
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Ensure aspectRatio is properly typed
 | 
					 | 
				
			||||||
  const options = {
 | 
					  const options = {
 | 
				
			||||||
    controls: defaultOptions?.controls ?? true,
 | 
					    controls: defaultOptions?.controls ?? true,
 | 
				
			||||||
    autoplay: defaultOptions?.autoplay ?? false,
 | 
					    autoplay: defaultOptions?.autoplay ?? false,
 | 
				
			||||||
    muted: defaultOptions?.muted ?? false,
 | 
					    muted: defaultOptions?.muted ?? false,
 | 
				
			||||||
    loop: false,
 | 
					    loop: false,
 | 
				
			||||||
    preload: 'metadata' as const,
 | 
					    preload: 'metadata' as const,
 | 
				
			||||||
    aspectRatio: defaultOptions?.aspectRatio ?? '16:9' as const,
 | 
					    aspectRatio: (defaultOptions?.aspectRatio ?? '16:9') as '16:9' | '4:3' | '1:1',
 | 
				
			||||||
    showMetadata: defaultOptions?.showMetadata ?? true
 | 
					    showMetadata: defaultOptions?.showMetadata ?? true
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					  const html = await buildProcessedVideoHTML(src, metadata, options);
 | 
				
			||||||
  return createVideoElement(src, metadata, options);
 | 
					  return { type: 'html', value: html };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function processLinkAsVideo(node: any, defaultOptions: VideoConfig['defaultOptions']): any | null {
 | 
					 | 
				
			||||||
  const src = node.url;
 | 
					 | 
				
			||||||
  const metadata = {
 | 
					 | 
				
			||||||
    title: node.title || extractTextContent(node),
 | 
					 | 
				
			||||||
    description: node.title
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Ensure aspectRatio is properly typed
 | 
					async function processLinkAsVideoAsync(node: any, defaultOptions: VideoConfig['defaultOptions']): Promise<any | null> {
 | 
				
			||||||
 | 
					  const src = node.url;
 | 
				
			||||||
 | 
					  const metadata = { title: node.title || extractTextContent(node), description: node.title };
 | 
				
			||||||
  const options = {
 | 
					  const options = {
 | 
				
			||||||
    controls: defaultOptions?.controls ?? true,
 | 
					    controls: defaultOptions?.controls ?? true,
 | 
				
			||||||
    autoplay: defaultOptions?.autoplay ?? false,
 | 
					    autoplay: defaultOptions?.autoplay ?? false,
 | 
				
			||||||
    muted: defaultOptions?.muted ?? false,
 | 
					    muted: defaultOptions?.muted ?? false,
 | 
				
			||||||
    loop: false,
 | 
					    loop: false,
 | 
				
			||||||
    preload: 'metadata' as const,
 | 
					    preload: 'metadata' as const,
 | 
				
			||||||
    aspectRatio: defaultOptions?.aspectRatio ?? '16:9' as const,
 | 
					    aspectRatio: (defaultOptions?.aspectRatio ?? '16:9') as '16:9' | '4:3' | '1:1',
 | 
				
			||||||
 | 
					    showMetadata: defaultOptions?.showMetadata ?? true
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  const html = await buildProcessedVideoHTML(src, metadata, options);
 | 
				
			||||||
 | 
					  return { type: 'html', value: html };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function processHTMLVideoAsync(node: any, defaultOptions: VideoConfig['defaultOptions']): Promise<any | null> {
 | 
				
			||||||
 | 
					  const htmlContent = node.value || '';
 | 
				
			||||||
 | 
					  const srcMatch = htmlContent.match(/src=["']([^"']+)["']/);
 | 
				
			||||||
 | 
					  if (!srcMatch) return null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const titleMatch = htmlContent.match(/title=["']([^"']+)["']/);
 | 
				
			||||||
 | 
					  const posterMatch = htmlContent.match(/poster=["']([^"']+)["']/);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const src = srcMatch[1];
 | 
				
			||||||
 | 
					  const metadata = { title: titleMatch?.[1], poster: posterMatch?.[1] };
 | 
				
			||||||
 | 
					  const options = {
 | 
				
			||||||
 | 
					    controls: htmlContent.includes('controls'),
 | 
				
			||||||
 | 
					    autoplay: htmlContent.includes('autoplay'),
 | 
				
			||||||
 | 
					    muted: htmlContent.includes('muted'),
 | 
				
			||||||
 | 
					    loop: htmlContent.includes('loop'),
 | 
				
			||||||
 | 
					    preload: 'metadata' as const,
 | 
				
			||||||
 | 
					    aspectRatio: (defaultOptions?.aspectRatio ?? '16:9') as '16:9' | '4:3' | '1:1',
 | 
				
			||||||
    showMetadata: defaultOptions?.showMetadata ?? true
 | 
					    showMetadata: defaultOptions?.showMetadata ?? true
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return createVideoElement(src, metadata, options);
 | 
					  const html = await buildProcessedVideoHTML(src, metadata, options);
 | 
				
			||||||
 | 
					  return { type: 'html', value: html };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function processHTMLVideo(node: any, defaultOptions: VideoConfig['defaultOptions']): any | null {
 | 
					function buildProcessedVideoHTML(src: string, metadata: any, options: {
 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    // Parse HTML to extract video attributes
 | 
					 | 
				
			||||||
    const htmlContent = node.value;
 | 
					 | 
				
			||||||
    const srcMatch = htmlContent.match(/src=["']([^"']+)["']/);
 | 
					 | 
				
			||||||
    const titleMatch = htmlContent.match(/title=["']([^"']+)["']/);
 | 
					 | 
				
			||||||
    const posterMatch = htmlContent.match(/poster=["']([^"']+)["']/);
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    if (!srcMatch) {
 | 
					 | 
				
			||||||
      return null;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const src = srcMatch[1];
 | 
					 | 
				
			||||||
    const metadata = {
 | 
					 | 
				
			||||||
      title: titleMatch?.[1],
 | 
					 | 
				
			||||||
      poster: posterMatch?.[1]
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const options = {
 | 
					 | 
				
			||||||
      controls: htmlContent.includes('controls'),
 | 
					 | 
				
			||||||
      autoplay: htmlContent.includes('autoplay'),
 | 
					 | 
				
			||||||
      muted: htmlContent.includes('muted'),
 | 
					 | 
				
			||||||
      loop: htmlContent.includes('loop'),
 | 
					 | 
				
			||||||
      preload: 'metadata' as const,
 | 
					 | 
				
			||||||
      aspectRatio: defaultOptions?.aspectRatio ?? '16:9' as const,
 | 
					 | 
				
			||||||
      showMetadata: defaultOptions?.showMetadata ?? true
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return createVideoElement(src, metadata, options);
 | 
					 | 
				
			||||||
  } catch (error) {
 | 
					 | 
				
			||||||
    console.warn('[VIDEO PLUGIN] Failed to process HTML video:', error);
 | 
					 | 
				
			||||||
    return null;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function createVideoElement(src: string, metadata: any, options: {
 | 
					 | 
				
			||||||
  controls: boolean;
 | 
					  controls: boolean;
 | 
				
			||||||
  autoplay: boolean;
 | 
					  autoplay: boolean;
 | 
				
			||||||
  muted: boolean;
 | 
					  muted: boolean;
 | 
				
			||||||
 | 
				
			|||||||
@ -147,37 +147,42 @@ export class VideoProcessor {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
 | 
					  // identifyVideoSource(url: string)
 | 
				
			||||||
  private identifyVideoSource(url: string): { type: VideoSource['type']; url: string } {
 | 
					  private identifyVideoSource(url: string): { type: VideoSource['type']; url: string } {
 | 
				
			||||||
    console.log(`[VIDEO PROCESSOR] Identifying source for URL: ${url}`);
 | 
					    console.log(`[VIDEO PROCESSOR] Identifying source for URL: ${url}`);
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    // Nextcloud share links
 | 
					    // Treat links that point at your configured MinIO endpoint as 'minio'
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const minioHost = process.env.MINIO_URL ? new URL(process.env.MINIO_URL).host : null;
 | 
				
			||||||
 | 
					      const urlHost = new URL(url).host;
 | 
				
			||||||
 | 
					      if (minioHost && urlHost === minioHost) {
 | 
				
			||||||
 | 
					        console.log(`[VIDEO PROCESSOR] Detected MinIO by endpoint host match: ${minioHost}`);
 | 
				
			||||||
 | 
					        return { type: 'minio', url };
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch {
 | 
				
			||||||
 | 
					      /* ignore URL parse errors */
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (url.includes('/s/') || url.includes('nextcloud') || url.includes('owncloud')) {
 | 
					    if (url.includes('/s/') || url.includes('nextcloud') || url.includes('owncloud')) {
 | 
				
			||||||
      console.log(`[VIDEO PROCESSOR] Detected Nextcloud share link`);
 | 
					      console.log(`[VIDEO PROCESSOR] Detected Nextcloud share link`);
 | 
				
			||||||
      return { type: 'nextcloud', url };
 | 
					      return { type: 'nextcloud', url };
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    // S3 URLs
 | 
					 | 
				
			||||||
    if (url.includes('amazonaws.com') || url.includes('.s3.')) {
 | 
					    if (url.includes('amazonaws.com') || url.includes('.s3.')) {
 | 
				
			||||||
      console.log(`[VIDEO PROCESSOR] Detected S3 URL`);
 | 
					      console.log(`[VIDEO PROCESSOR] Detected S3 URL`);
 | 
				
			||||||
      return { type: 's3', url };
 | 
					      return { type: 's3', url };
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    // Minio URLs (common patterns)
 | 
					 | 
				
			||||||
    if (url.includes('minio') || url.match(/:\d+\/[^\/]+\/.*\.(mp4|webm|ogg|mov|avi)/i)) {
 | 
					    if (url.includes('minio') || url.match(/:\d+\/[^\/]+\/.*\.(mp4|webm|ogg|mov|avi)/i)) {
 | 
				
			||||||
      console.log(`[VIDEO PROCESSOR] Detected Minio URL`);
 | 
					      console.log(`[VIDEO PROCESSOR] Detected Minio URL`);
 | 
				
			||||||
      return { type: 'minio', url };
 | 
					      return { type: 'minio', url };
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    // Local files (relative paths)
 | 
					 | 
				
			||||||
    if (url.startsWith('/') && !url.startsWith('http')) {
 | 
					    if (url.startsWith('/') && !url.startsWith('http')) {
 | 
				
			||||||
      console.log(`[VIDEO PROCESSOR] Detected local file path`);
 | 
					      console.log(`[VIDEO PROCESSOR] Detected local file path`);
 | 
				
			||||||
      return { type: 'local', url };
 | 
					      return { type: 'local', url };
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    // Direct HTTP(S) URLs
 | 
					 | 
				
			||||||
    console.log(`[VIDEO PROCESSOR] Detected direct HTTP URL`);
 | 
					    console.log(`[VIDEO PROCESSOR] Detected direct HTTP URL`);
 | 
				
			||||||
    return { type: 'direct', url };
 | 
					    return { type: 'direct', url };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
  private async processNextcloudVideo(url: string): Promise<VideoSource[]> {
 | 
					  private async processNextcloudVideo(url: string): Promise<VideoSource[]> {
 | 
				
			||||||
    console.log(`[VIDEO PROCESSOR] Processing Nextcloud video: ${url}`);
 | 
					    console.log(`[VIDEO PROCESSOR] Processing Nextcloud video: ${url}`);
 | 
				
			||||||
@ -262,7 +267,7 @@ export class VideoProcessor {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
  // Replace processS3MinioVideo() with:
 | 
					  // Strengthen the MinIO pre-signing to force a clean, streamable response
 | 
				
			||||||
  private async processS3MinioVideo(url: string, type: 's3' | 'minio'): Promise<VideoSource[]> {
 | 
					  private async processS3MinioVideo(url: string, type: 's3' | 'minio'): Promise<VideoSource[]> {
 | 
				
			||||||
    console.log(`[VIDEO PROCESSOR] Processing ${type} video: ${url}`);
 | 
					    console.log(`[VIDEO PROCESSOR] Processing ${type} video: ${url}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -270,18 +275,25 @@ export class VideoProcessor {
 | 
				
			|||||||
      const parsed = this.normalizeMinioUrl(url);
 | 
					      const parsed = this.normalizeMinioUrl(url);
 | 
				
			||||||
      if (parsed) {
 | 
					      if (parsed) {
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
          const cmd = new GetObjectCommand({ Bucket: parsed.bucket, Key: parsed.key });
 | 
					          const mime = this.getMimeType(url);
 | 
				
			||||||
          const signed = await getSignedUrl(this.s3Client, cmd, { expiresIn: 3600 }); // 1h
 | 
					          const cmd = new GetObjectCommand({
 | 
				
			||||||
 | 
					            Bucket: parsed.bucket,
 | 
				
			||||||
 | 
					            Key: parsed.key,
 | 
				
			||||||
 | 
					            ResponseContentType: mime,                       // ensure video/* MIME
 | 
				
			||||||
 | 
					            ResponseContentDisposition: 'inline',            // avoid download
 | 
				
			||||||
 | 
					            ResponseCacheControl: 'public, max-age=3600'     // optional, helps caching
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					          const signed = await getSignedUrl(this.s3Client, cmd, { expiresIn: 3600 });
 | 
				
			||||||
          return [{ type: 'minio', url: signed, originalUrl: url, cached: false }];
 | 
					          return [{ type: 'minio', url: signed, originalUrl: url, cached: false }];
 | 
				
			||||||
        } catch (e) {
 | 
					        } catch (e) {
 | 
				
			||||||
          console.warn('[VIDEO PROCESSOR] MinIO pre-sign failed:', e);
 | 
					          console.warn('[VIDEO PROCESSOR] MinIO pre-sign failed:', e);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    return [{ type, url, cached: false }];
 | 
					    return [{ type, url, cached: false }];
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private normalizeMinioUrl(inputUrl: string): { bucket: string; key: string } | null {
 | 
					  private normalizeMinioUrl(inputUrl: string): { bucket: string; key: string } | null {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const u = new URL(inputUrl);
 | 
					      const u = new URL(inputUrl);
 | 
				
			||||||
@ -580,16 +592,27 @@ export class VideoProcessor {
 | 
				
			|||||||
    
 | 
					    
 | 
				
			||||||
    console.log(`[VIDEO PROCESSOR] Primary source: ${primarySource.type} (cached: ${primarySource.cached})`);
 | 
					    console.log(`[VIDEO PROCESSOR] Primary source: ${primarySource.type} (cached: ${primarySource.cached})`);
 | 
				
			||||||
    console.log(`[VIDEO PROCESSOR] Primary source URL: ${primarySource.url}`);
 | 
					    console.log(`[VIDEO PROCESSOR] Primary source URL: ${primarySource.url}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const needsCrossOrigin = processedVideo.sources.some(s =>
 | 
				
			||||||
 | 
					      /^https?:\/\//i.test(s.url) && !this.isSameOrigin(s.url)
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
 | 
					    if (needsCrossOrigin==true){
 | 
				
			||||||
 | 
					      console.log('FUCK CROSS ORIGIN')
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // In generateVideoHTML(...), when building videoAttributes array
 | 
				
			||||||
    let videoAttributes = [
 | 
					    let videoAttributes = [
 | 
				
			||||||
      controls ? 'controls' : '',
 | 
					      controls ? 'controls' : '',
 | 
				
			||||||
      autoplay ? 'autoplay' : '',
 | 
					      autoplay ? 'autoplay' : '',
 | 
				
			||||||
      muted ? 'muted' : '',
 | 
					      muted ? 'muted' : '',
 | 
				
			||||||
      loop ? 'loop' : '',
 | 
					      loop ? 'loop' : '',
 | 
				
			||||||
 | 
					      needsCrossOrigin ? 'crossorigin="anonymous"' : '',
 | 
				
			||||||
      `preload="${preload}"`,
 | 
					      `preload="${preload}"`,
 | 
				
			||||||
      metadata.poster ? `poster="${metadata.poster}"` : '',
 | 
					      metadata.poster ? `poster="${metadata.poster}"` : '',
 | 
				
			||||||
      `data-video-title="${metadata.title || 'Embedded Video'}"`
 | 
					      `data-video-title="${metadata.title || 'Embedded Video'}"`
 | 
				
			||||||
    ].filter(Boolean).join(' ');
 | 
					    ].filter(Boolean).join(' ');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    console.log(`[VIDEO PROCESSOR] Video attributes: ${videoAttributes}`);
 | 
					    console.log(`[VIDEO PROCESSOR] Video attributes: ${videoAttributes}`);
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
@ -626,24 +649,40 @@ export class VideoProcessor {
 | 
				
			|||||||
    
 | 
					    
 | 
				
			||||||
    return finalHTML;
 | 
					    return finalHTML;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  
 | 
					
 | 
				
			||||||
  private getMimeType(url: string): string {
 | 
					  private isSameOrigin(u: string): boolean {
 | 
				
			||||||
    const extension = url.split('.').pop()?.toLowerCase();
 | 
					    try {
 | 
				
			||||||
    const mimeTypes: Record<string, string> = {
 | 
					      const site = process.env.PUBLIC_SITE_ORIGIN || '';
 | 
				
			||||||
      'mp4': 'video/mp4',
 | 
					      if (!site) return false;
 | 
				
			||||||
      'webm': 'video/webm',
 | 
					      return new URL(u).host === new URL(site).host;
 | 
				
			||||||
      'ogg': 'video/ogg',
 | 
					    } catch {
 | 
				
			||||||
      'mov': 'video/quicktime',
 | 
					      return false;
 | 
				
			||||||
      'avi': 'video/x-msvideo',
 | 
					    }
 | 
				
			||||||
      'm4v': 'video/m4v'
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    const mimeType = mimeTypes[extension || ''] || 'video/mp4';
 | 
					 | 
				
			||||||
    console.log(`[VIDEO PROCESSOR] MIME type for extension '${extension}': ${mimeType}`);
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    return mimeType;
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
 | 
					  // Make getMimeType() robust against presigned URLs with query strings
 | 
				
			||||||
 | 
					  private getMimeType(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'
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    const mimeType = (extension && mimeTypes[extension]) ? mimeTypes[extension] : 'video/mp4';
 | 
				
			||||||
 | 
					    console.log(`[VIDEO PROCESSOR] MIME type for extension '${extension}': ${mimeType}`);
 | 
				
			||||||
 | 
					    return mimeType;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private formatDuration(seconds: number): string {
 | 
					  private formatDuration(seconds: number): string {
 | 
				
			||||||
    const hours = Math.floor(seconds / 3600);
 | 
					    const hours = Math.floor(seconds / 3600);
 | 
				
			||||||
    const minutes = Math.floor((seconds % 3600) / 60);
 | 
					    const minutes = Math.floor((seconds % 3600) / 60);
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user