videos
This commit is contained in:
		
							parent
							
								
									f159f904f0
								
							
						
					
					
						commit
						2d920391ad
					
				@ -18,6 +18,9 @@ sections:
 | 
			
		||||
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!
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -24,6 +24,7 @@ interface VideoConfig {
 | 
			
		||||
 * 3. HTML video tags: <video src="url"></video>
 | 
			
		||||
 * 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 = {}) => {
 | 
			
		||||
  const {
 | 
			
		||||
    enableAsync = true,
 | 
			
		||||
@ -37,84 +38,75 @@ export const remarkVideoPlugin: Plugin<[VideoConfig?], Root> = (config = {}) =>
 | 
			
		||||
  } = config;
 | 
			
		||||
 | 
			
		||||
  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) => {
 | 
			
		||||
      if (node.name === 'video' && typeof index === 'number') {
 | 
			
		||||
        const videoNode = processVideoDirective(node, defaultOptions);
 | 
			
		||||
        if (videoNode) {
 | 
			
		||||
          videoNodes.push({ node, parent, index, replacement: videoNode });
 | 
			
		||||
        }
 | 
			
		||||
        tasks.push((async () => {
 | 
			
		||||
          const replacement = await processVideoDirectiveAsync(node, defaultOptions);
 | 
			
		||||
          if (replacement && parent?.children) parent.children[index] = replacement;
 | 
			
		||||
        })());
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Find container directives (:::video ... :::)
 | 
			
		||||
    // :::video ... :::
 | 
			
		||||
    visit(tree, 'containerDirective', (node: any, index: number | undefined, parent: any) => {
 | 
			
		||||
      if (node.name === 'video' && typeof index === 'number') {
 | 
			
		||||
        const videoNode = processVideoDirective(node, defaultOptions);
 | 
			
		||||
        if (videoNode) {
 | 
			
		||||
          videoNodes.push({ node, parent, index, replacement: videoNode });
 | 
			
		||||
        }
 | 
			
		||||
        tasks.push((async () => {
 | 
			
		||||
          const replacement = await processVideoDirectiveAsync(node, defaultOptions);
 | 
			
		||||
          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) => {
 | 
			
		||||
      if (isVideoUrl(node.url) && typeof index === 'number') {
 | 
			
		||||
        const videoNode = processImageAsVideo(node, defaultOptions);
 | 
			
		||||
        if (videoNode) {
 | 
			
		||||
          videoNodes.push({ node, parent, index, replacement: videoNode });
 | 
			
		||||
        }
 | 
			
		||||
        tasks.push((async () => {
 | 
			
		||||
          const replacement = await processImageAsVideoAsync(node, defaultOptions);
 | 
			
		||||
          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) => {
 | 
			
		||||
      if (isVideoUrl(node.url) && typeof index === 'number') {
 | 
			
		||||
        const videoNode = processLinkAsVideo(node, defaultOptions);
 | 
			
		||||
        if (videoNode) {
 | 
			
		||||
          videoNodes.push({ node, parent, index, replacement: videoNode });
 | 
			
		||||
        }
 | 
			
		||||
        tasks.push((async () => {
 | 
			
		||||
          const replacement = await processLinkAsVideoAsync(node, defaultOptions);
 | 
			
		||||
          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) => {
 | 
			
		||||
      if (node.value && node.value.includes('<video') && typeof index === 'number') {
 | 
			
		||||
        const videoNode = processHTMLVideo(node, defaultOptions);
 | 
			
		||||
        if (videoNode) {
 | 
			
		||||
          videoNodes.push({ node, parent, index, replacement: videoNode });
 | 
			
		||||
        }
 | 
			
		||||
        tasks.push((async () => {
 | 
			
		||||
          const replacement = await processHTMLVideoAsync(node, defaultOptions);
 | 
			
		||||
          if (replacement && parent?.children) parent.children[index] = replacement;
 | 
			
		||||
        })());
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Replace all found video nodes
 | 
			
		||||
    for (const { parent, index, replacement } of videoNodes.reverse()) {
 | 
			
		||||
      if (parent && parent.children && Array.isArray(parent.children)) {
 | 
			
		||||
        parent.children[index] = replacement;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    await Promise.all(tasks);
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
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 src = attributes.src;
 | 
			
		||||
  
 | 
			
		||||
  if (!src) {
 | 
			
		||||
    console.warn('[VIDEO PLUGIN] Video directive missing src attribute');
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Validate and normalize aspect ratio
 | 
			
		||||
  const aspectRatioValue = attributes.aspectRatio || attributes['aspect-ratio'] || defaultOptions?.aspectRatio;
 | 
			
		||||
  const validAspectRatios = ['16:9', '4:3', '1:1'] as const;
 | 
			
		||||
  const aspectRatio = validAspectRatios.includes(aspectRatioValue as any) 
 | 
			
		||||
    ? aspectRatioValue as '16:9' | '4:3' | '1:1'
 | 
			
		||||
    : '16:9';
 | 
			
		||||
  const aspectRatio = (validAspectRatios.includes(aspectRatioValue as any) ? aspectRatioValue : '16:9') as '16:9' | '4:3' | '1:1';
 | 
			
		||||
 | 
			
		||||
  // Extract options from attributes
 | 
			
		||||
  const options = {
 | 
			
		||||
    controls: attributes.controls !== undefined ? attributes.controls !== 'false' : defaultOptions?.controls ?? true,
 | 
			
		||||
    autoplay: attributes.autoplay !== undefined ? attributes.autoplay !== 'false' : defaultOptions?.autoplay ?? false,
 | 
			
		||||
@ -131,87 +123,68 @@ function processVideoDirective(node: any, defaultOptions: VideoConfig['defaultOp
 | 
			
		||||
    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 metadata = {
 | 
			
		||||
    title: node.title || node.alt,
 | 
			
		||||
    description: node.alt
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // Ensure aspectRatio is properly typed
 | 
			
		||||
  const metadata = { title: node.title || node.alt, description: node.alt };
 | 
			
		||||
  const options = {
 | 
			
		||||
    controls: defaultOptions?.controls ?? true,
 | 
			
		||||
    autoplay: defaultOptions?.autoplay ?? false,
 | 
			
		||||
    muted: defaultOptions?.muted ?? false,
 | 
			
		||||
    loop: false,
 | 
			
		||||
    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
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return createVideoElement(src, metadata, options);
 | 
			
		||||
  const html = await buildProcessedVideoHTML(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 = {
 | 
			
		||||
    controls: defaultOptions?.controls ?? true,
 | 
			
		||||
    autoplay: defaultOptions?.autoplay ?? false,
 | 
			
		||||
    muted: defaultOptions?.muted ?? false,
 | 
			
		||||
    loop: false,
 | 
			
		||||
    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
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  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 {
 | 
			
		||||
  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: {
 | 
			
		||||
function buildProcessedVideoHTML(src: string, metadata: any, options: {
 | 
			
		||||
  controls: boolean;
 | 
			
		||||
  autoplay: boolean;
 | 
			
		||||
  muted: boolean;
 | 
			
		||||
 | 
			
		||||
@ -147,38 +147,43 @@ export class VideoProcessor {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // identifyVideoSource(url: string)
 | 
			
		||||
  private identifyVideoSource(url: string): { type: VideoSource['type']; url: string } {
 | 
			
		||||
    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')) {
 | 
			
		||||
      console.log(`[VIDEO PROCESSOR] Detected Nextcloud share link`);
 | 
			
		||||
      return { type: 'nextcloud', url };
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // S3 URLs
 | 
			
		||||
    if (url.includes('amazonaws.com') || url.includes('.s3.')) {
 | 
			
		||||
      console.log(`[VIDEO PROCESSOR] Detected S3 URL`);
 | 
			
		||||
      return { type: 's3', url };
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Minio URLs (common patterns)
 | 
			
		||||
    if (url.includes('minio') || url.match(/:\d+\/[^\/]+\/.*\.(mp4|webm|ogg|mov|avi)/i)) {
 | 
			
		||||
      console.log(`[VIDEO PROCESSOR] Detected Minio URL`);
 | 
			
		||||
      return { type: 'minio', url };
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Local files (relative paths)
 | 
			
		||||
    if (url.startsWith('/') && !url.startsWith('http')) {
 | 
			
		||||
      console.log(`[VIDEO PROCESSOR] Detected local file path`);
 | 
			
		||||
      return { type: 'local', url };
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Direct HTTP(S) URLs
 | 
			
		||||
    console.log(`[VIDEO PROCESSOR] Detected direct HTTP URL`);
 | 
			
		||||
    return { type: 'direct', url };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  
 | 
			
		||||
  private async processNextcloudVideo(url: string): Promise<VideoSource[]> {
 | 
			
		||||
    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[]> {
 | 
			
		||||
    console.log(`[VIDEO PROCESSOR] Processing ${type} video: ${url}`);
 | 
			
		||||
 | 
			
		||||
@ -270,18 +275,25 @@ export class VideoProcessor {
 | 
			
		||||
      const parsed = this.normalizeMinioUrl(url);
 | 
			
		||||
      if (parsed) {
 | 
			
		||||
        try {
 | 
			
		||||
          const cmd = new GetObjectCommand({ Bucket: parsed.bucket, Key: parsed.key });
 | 
			
		||||
          const signed = await getSignedUrl(this.s3Client, cmd, { expiresIn: 3600 }); // 1h
 | 
			
		||||
          const mime = this.getMimeType(url);
 | 
			
		||||
          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 }];
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
          console.warn('[VIDEO PROCESSOR] MinIO pre-sign failed:', e);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return [{ type, url, cached: false }];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  private normalizeMinioUrl(inputUrl: string): { bucket: string; key: string } | null {
 | 
			
		||||
    try {
 | 
			
		||||
      const u = new URL(inputUrl);
 | 
			
		||||
@ -581,16 +593,27 @@ export class VideoProcessor {
 | 
			
		||||
    console.log(`[VIDEO PROCESSOR] Primary source: ${primarySource.type} (cached: ${primarySource.cached})`);
 | 
			
		||||
    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 = [
 | 
			
		||||
      controls ? 'controls' : '',
 | 
			
		||||
      autoplay ? 'autoplay' : '',
 | 
			
		||||
      muted ? 'muted' : '',
 | 
			
		||||
      loop ? 'loop' : '',
 | 
			
		||||
      needsCrossOrigin ? 'crossorigin="anonymous"' : '',
 | 
			
		||||
      `preload="${preload}"`,
 | 
			
		||||
      metadata.poster ? `poster="${metadata.poster}"` : '',
 | 
			
		||||
      `data-video-title="${metadata.title || 'Embedded Video'}"`
 | 
			
		||||
    ].filter(Boolean).join(' ');
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    console.log(`[VIDEO PROCESSOR] Video attributes: ${videoAttributes}`);
 | 
			
		||||
    
 | 
			
		||||
    const sourceTags = processedVideo.sources
 | 
			
		||||
@ -627,23 +650,39 @@ export class VideoProcessor {
 | 
			
		||||
    return finalHTML;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private isSameOrigin(u: string): boolean {
 | 
			
		||||
    try {
 | 
			
		||||
      const site = process.env.PUBLIC_SITE_ORIGIN || '';
 | 
			
		||||
      if (!site) return false;
 | 
			
		||||
      return new URL(u).host === new URL(site).host;
 | 
			
		||||
    } catch {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // Make getMimeType() robust against presigned URLs with query strings
 | 
			
		||||
  private getMimeType(url: string): string {
 | 
			
		||||
    const extension = url.split('.').pop()?.toLowerCase();
 | 
			
		||||
    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'
 | 
			
		||||
      mp4: 'video/mp4',
 | 
			
		||||
      webm: 'video/webm',
 | 
			
		||||
      ogg: 'video/ogg',
 | 
			
		||||
      mov: 'video/quicktime',
 | 
			
		||||
      avi: 'video/x-msvideo',
 | 
			
		||||
      m4v: 'video/m4v'
 | 
			
		||||
    };
 | 
			
		||||
    
 | 
			
		||||
    const mimeType = mimeTypes[extension || ''] || 'video/mp4';
 | 
			
		||||
    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 {
 | 
			
		||||
    const hours = Math.floor(seconds / 3600);
 | 
			
		||||
    const minutes = Math.floor((seconds % 3600) / 60);
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user