feat: Update video thumbnail handling in viewer frontend #10
@ -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 = `
|
||||
<svg width="400" height="400" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="400" height="400" fill="#e5e7eb"/>
|
||||
<circle cx="200" cy="200" r="40" fill="#6b7280" opacity="0.8"/>
|
||||
<polygon points="190,185 190,215 215,200" fill="white"/>
|
||||
</svg>
|
||||
`.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 = `
|
||||
<svg width="400" height="400" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="400" height="400" fill="#e5e7eb"/>
|
||||
<circle cx="200" cy="200" r="40" fill="#6b7280" opacity="0.8"/>
|
||||
<polygon points="190,185 190,215 215,200" fill="white"/>
|
||||
</svg>
|
||||
`.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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user