diff --git a/services/worker/src/workers/proxy.js b/services/worker/src/workers/proxy.js index 95f8467..f001156 100644 --- a/services/worker/src/workers/proxy.js +++ b/services/worker/src/workers/proxy.js @@ -39,7 +39,9 @@ async function loadProxyEncodingSettings() { // Codec names ffprobe reports for still-image inputs. These bypass the video // transcode entirely — see proxyWorker below. -const IMAGE_CODECS = new Set(['png', 'mjpeg', 'jpeg', 'webp', 'gif', 'tiff', 'bmp', 'jpegls']); +const IMAGE_CODECS = new Set([ + 'png', 'mjpeg', 'jpeg', 'webp', 'gif', 'tiff', 'bmp', 'jpegls', 'svg', +]); const S3_BUCKET = process.env.S3_BUCKET || 'wild-dragon'; @@ -66,13 +68,26 @@ export const proxyWorker = async (job) => { console.log(`[proxy] Downloading ${inputKey} for asset ${assetId}`); await downloadFromS3(S3_BUCKET, inputKey, inputPath); + // Look up the asset row early — we want media_type before deciding how + // to process. That lets us route 'image' assets to the poster path even + // when ffprobe doesn't return a codec name in IMAGE_CODECS (e.g. future + // formats like AVIF / HEIF / JPEG-XL). + const assetRow = await query( + 'SELECT media_type FROM assets WHERE id = $1', + [assetId] + ); + const dbMediaType = assetRow.rows[0]?.media_type || null; + // Reject obviously-empty inputs before handing them to ffmpeg. Aborted // SRT/RTMP recordings end up as 0-byte (or ftyp-only ~1KB) objects in S3 // when the source disconnects before any frame is received; the proxy // pipeline used to bomb on "moov atom not found", which buried the // real reason. Bail with a clear message and let the asset go to 'error'. + // + // Skip this check for image assets — a single PNG icon can legitimately + // be a few hundred bytes. const { size: inputBytes } = await stat(inputPath); - if (inputBytes < 4096) { + if (dbMediaType !== 'image' && inputBytes < 4096) { throw new Error( `Source is empty or truncated (${inputBytes} bytes). The recording ` + `likely ended before any frames were received — check the source ` + @@ -95,12 +110,17 @@ export const proxyWorker = async (job) => { // Still images skip the video proxy — they have no temporal stream and // x264 with a one-frame PNG input fails (Could not open encoder before EOF). // Generate a scaled JPEG poster instead; the thumbnail job will downsize it. - const isImage = mediaInfo.codec && IMAGE_CODECS.has(mediaInfo.codec.toLowerCase()); + // + // We treat the input as an image if EITHER the DB says so (media_type = + // 'image', set by upload.js based on Content-Type or extension), OR + // ffprobe reports a codec we know is a still-image format. + const codecLower = mediaInfo.codec ? mediaInfo.codec.toLowerCase() : null; + const isImage = dbMediaType === 'image' || (codecLower && IMAGE_CODECS.has(codecLower)); if (isImage) { const imageOutputKey = outputKey.replace(/\.mp4$/, '.jpg'); const imageOutputPath = outputPath.replace(/\.mp4$/, '.jpg'); - console.log(`[proxy] Image asset ${assetId} (${mediaInfo.codec}) — emitting poster instead of video proxy`); + console.log(`[proxy] Image asset ${assetId} (codec=${codecLower || 'unknown'}, db_media_type=${dbMediaType}) — emitting poster instead of video proxy`); await job.updateProgress(40); await transcodeImage(inputPath, imageOutputPath); await job.updateProgress(70);