feat: client-side video thumbnail generation (no ffmpeg)
Backend:
- get_thumbnail: for non-images, serve cached thumb or 204 (not 400)
- store_external_thumbnail: validate + re-encode to WebP + persist
- get_cached_thumbnail: memory → disk lookup without generation
- PUT /api/files/{id}/thumbnail/{size} endpoint for client uploads
- ThumbnailPort trait: add get_cached_thumbnail + store_external_thumbnail
Frontend (photos.js):
- On <img> error for video tiles, use <video> + <canvas> to extract
a frame at 25% duration using browser's native codec
- Show frame immediately via blob URL
- Fire-and-forget PUT to server for permanent caching
- Subsequent visits serve cached WebP instantly (no re-extraction)
Zero server-side dependencies — CPU distributed across clients.
This commit is contained in:
@@ -84,6 +84,27 @@ pub trait ThumbnailPort: Send + Sync + 'static {
|
||||
/// Delete all thumbnails for a file.
|
||||
async fn delete_thumbnails(&self, file_id: &str) -> Result<(), DomainError>;
|
||||
|
||||
/// Try to get a cached thumbnail without generating one.
|
||||
///
|
||||
/// Returns `None` if no cached thumbnail exists on disk or in memory.
|
||||
/// Used for non-image file types (videos) where thumbnails are
|
||||
/// generated client-side and uploaded.
|
||||
async fn get_cached_thumbnail(
|
||||
&self,
|
||||
file_id: &str,
|
||||
size: ThumbnailSize,
|
||||
) -> Option<Bytes>;
|
||||
|
||||
/// Store an externally-generated thumbnail (e.g. client-side video frame).
|
||||
///
|
||||
/// Validates the image, re-encodes to WebP, and persists to cache.
|
||||
async fn store_external_thumbnail(
|
||||
&self,
|
||||
file_id: &str,
|
||||
size: ThumbnailSize,
|
||||
data: Bytes,
|
||||
) -> Result<Bytes, DomainError>;
|
||||
|
||||
/// Get cache statistics.
|
||||
async fn get_stats(&self) -> ThumbnailStatsDto;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user