diff --git a/services/worker/src/workers/thumbnail.js b/services/worker/src/workers/thumbnail.js index fb0117a..ac7ca00 100644 --- a/services/worker/src/workers/thumbnail.js +++ b/services/worker/src/workers/thumbnail.js @@ -3,10 +3,26 @@ import { unlink } from 'fs/promises'; import { tmpdir } from 'os'; import { query } from '../db/client.js'; import { downloadFromS3, uploadToS3 } from '../s3/client.js'; -import { extractFrameAtTime } from '../ffmpeg/executor.js'; +import { extractFrameAtTime, getMediaDuration } from '../ffmpeg/executor.js'; const S3_BUCKET = process.env.S3_BUCKET || 'wild-dragon'; +/** + * Pick a seek time that won't exceed the clip duration. + * Tries 5s first, then 1s, then 0s as a last resort. + */ +async function pickSeekTime(inputPath) { + try { + const duration = await getMediaDuration(inputPath); + if (duration >= 5) return '00:00:05'; + if (duration >= 1) return '00:00:01'; + return '00:00:00'; + } catch { + // If ffprobe fails, fall back to 5s and let ffmpeg handle it + return '00:00:05'; + } +} + export const thumbnailWorker = async (job) => { const { assetId, proxyKey, outputKey } = job.data; @@ -16,22 +32,25 @@ export const thumbnailWorker = async (job) => { try { // Download proxy from S3 - job.updateProgress(10); + await job.updateProgress(10); console.log(`[thumbnail] Downloading ${proxyKey} for asset ${assetId}`); await downloadFromS3(S3_BUCKET, proxyKey, inputPath); - // Extract frame at 5 seconds (or start if clip is short) - job.updateProgress(40); - console.log(`[thumbnail] Extracting frame for asset ${assetId}`); - await extractFrameAtTime(inputPath, outputPath, '00:00:05'); + // Pick a safe seek time based on actual clip duration + const seekTime = await pickSeekTime(inputPath); + + // Extract frame + await job.updateProgress(40); + console.log(`[thumbnail] Extracting frame at ${seekTime} for asset ${assetId}`); + await extractFrameAtTime(inputPath, outputPath, seekTime); // Upload thumbnail to S3 - job.updateProgress(70); + await job.updateProgress(70); console.log(`[thumbnail] Uploading to ${outputKey}`); await uploadToS3(S3_BUCKET, outputKey, outputPath); // Update asset: thumbnail key + mark ready - job.updateProgress(90); + await job.updateProgress(90); await query( `UPDATE assets SET thumbnail_s3_key = $1, status = 'ready', updated_at = NOW() @@ -39,14 +58,13 @@ export const thumbnailWorker = async (job) => { [outputKey, assetId] ); - job.updateProgress(100); + await job.updateProgress(100); console.log(`[thumbnail] Asset ${assetId} thumbnail complete → status=ready`); return { assetId, outputKey }; } catch (error) { console.error(`[thumbnail] Error processing asset ${assetId}:`, error); - // Don't set status=error just because thumbnail failed — asset is still usable - // Just log and let it be retried + // Don't set status=error for thumbnail failure — asset is still usable without it throw error; } finally { await Promise.all([