From 830c7bcaa60f09d9026abc356f0e9ef816104134 Mon Sep 17 00:00:00 2001 From: tanyar09 Date: Wed, 28 Jan 2026 17:55:58 +0000 Subject: [PATCH] feat: Update video thumbnail handling in viewer frontend - Refactored video thumbnail generation logic to fetch thumbnails from the backend instead of generating them locally. - Removed unused placeholder generation code for remote video URLs. - Improved error handling for backend thumbnail fetch failures, returning appropriate responses. - Enhanced caching strategy for fetched thumbnails to optimize performance. These changes streamline the video thumbnail process and improve the overall user experience in the viewer interface. --- .../app/api/photos/[id]/image/route.ts | 112 ++++-------------- 1 file changed, 24 insertions(+), 88 deletions(-) diff --git a/viewer-frontend/app/api/photos/[id]/image/route.ts b/viewer-frontend/app/api/photos/[id]/image/route.ts index 28e7e3f..e73e78e 100644 --- a/viewer-frontend/app/api/photos/[id]/image/route.ts +++ b/viewer-frontend/app/api/photos/[id]/image/route.ts @@ -4,7 +4,6 @@ import { readFile } from 'fs/promises'; import { createReadStream } from 'fs'; import { existsSync, statSync } from 'fs'; import path from 'path'; -import { getVideoThumbnail } from '@/lib/video-thumbnail'; // Conditionally import sharp - handle case where libvips is not installed let sharp: any = null; @@ -168,98 +167,35 @@ export async function GET( // Handle video thumbnail request if (thumbnail && mediaType === 'video') { - // Check if video path is a URL (can't generate thumbnail for remote videos) - const isVideoUrl = filePath.startsWith('http://') || filePath.startsWith('https://'); - - if (isVideoUrl) { - // For remote videos, we can't generate thumbnails locally - // Return a placeholder - console.warn(`Cannot generate thumbnail for remote video URL: ${filePath}`); - try { - if (!sharp) throw new Error('Sharp not available'); - const placeholderSvg = ` - - - - - - `.trim(); - - const placeholderBuffer = await sharp(Buffer.from(placeholderSvg)) - .png() - .toBuffer(); - - return new NextResponse(placeholderBuffer as unknown as BodyInit, { - headers: { - 'Content-Type': 'image/png', - 'Cache-Control': 'public, max-age=60', - }, - }); - } catch (error) { - console.error('Error generating placeholder for remote video:', error); - const minimalPng = Buffer.from( - 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==', - 'base64' - ); - return new NextResponse(minimalPng as unknown as BodyInit, { - headers: { - 'Content-Type': 'image/png', - 'Cache-Control': 'public, max-age=60', - }, - }); - } - } - + const backendBaseUrl = process.env.BACKEND_BASE_URL || 'http://127.0.0.1:8000'; + try { - const thumbnailBuffer = await getVideoThumbnail(filePath); - if (thumbnailBuffer && thumbnailBuffer.length > 0) { - return new NextResponse(thumbnailBuffer as unknown as BodyInit, { - headers: { - 'Content-Type': 'image/jpeg', - 'Cache-Control': 'public, max-age=31536000, immutable', - }, - }); - } - console.error(`Thumbnail generation returned empty buffer for video: ${filePath}`); - } catch (error) { - console.error(`Error generating thumbnail for video ${filePath}:`, error); - } - - // Fallback: return a placeholder image (gray box with play icon) - // Generate a PNG placeholder using sharp for better compatibility - try { - if (!sharp) throw new Error('Sharp not available'); - const placeholderSvg = ` - - - - - - `.trim(); - - const placeholderBuffer = await sharp(Buffer.from(placeholderSvg)) - .png() - .toBuffer(); - - return new NextResponse(placeholderBuffer as unknown as BodyInit, { - headers: { - 'Content-Type': 'image/png', - 'Cache-Control': 'public, max-age=60', // Short cache for placeholder - }, - }); - } catch (error) { - console.error('Error generating placeholder:', error); - // Ultimate fallback: return a minimal 1x1 PNG - const minimalPng = Buffer.from( - 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==', - 'base64' + const backendResponse = await fetch( + `${backendBaseUrl}/api/v1/videos/${photoId}/thumbnail`, + { headers: { Accept: 'image/*' } } ); - return new NextResponse(minimalPng as unknown as BodyInit, { + + if (!backendResponse.ok) { + console.error( + `Backend thumbnail fetch failed for video ${photoId}: ` + + `${backendResponse.status} ${backendResponse.statusText}` + ); + return new NextResponse('Failed to fetch video thumbnail', { status: 502 }); + } + + const arrayBuffer = await backendResponse.arrayBuffer(); + const contentType = backendResponse.headers.get('content-type') || 'image/jpeg'; + + return new NextResponse(arrayBuffer as unknown as BodyInit, { headers: { - 'Content-Type': 'image/png', - 'Cache-Control': 'public, max-age=60', + 'Content-Type': contentType, + // Keep caching modest: backend thumbnails can be regenerated/updated. + 'Cache-Control': 'public, max-age=86400', }, }); + } catch (error) { + console.error(`Error proxying backend thumbnail for video ${photoId}:`, error); + return new NextResponse('Failed to fetch video thumbnail', { status: 502 }); } }