fix(worker): route SVG (and other image assets) through the image-poster
path instead of failing the video transcode
Previously IMAGE_CODECS contained the raster ffprobe codec names ('png',
'mjpeg', 'jpeg', 'webp', 'gif', 'tiff', 'bmp', 'jpegls') but not 'svg'.
An SVG-as-asset (e.g. an architecture diagram dragged into a project) was
correctly tagged media_type='image' in the DB but ffprobe reported its
codec as 'svg', which fell through to the video branch, found
durationMs===null, and died with 'Empty or truncated source: codec=svg,
resolution=0x0'. That clogs the failed-jobs list with red rows that have
nothing to do with broken captures.
Two fixes here:
1) Add 'svg' to IMAGE_CODECS so the existing transcodeImage()/poster
path handles it.
2) Also bail to the poster path when the asset row itself says
media_type='image', even if ffprobe didn't return a codec name we
recognize (defensive — catches future formats like AVIF without
requiring an explicit catalog update).
Closes part of #13.
This commit is contained in:
parent
d07fb13401
commit
508e978fe5
1 changed files with 24 additions and 4 deletions
|
|
@ -39,7 +39,9 @@ async function loadProxyEncodingSettings() {
|
||||||
|
|
||||||
// Codec names ffprobe reports for still-image inputs. These bypass the video
|
// Codec names ffprobe reports for still-image inputs. These bypass the video
|
||||||
// transcode entirely — see proxyWorker below.
|
// 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';
|
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}`);
|
console.log(`[proxy] Downloading ${inputKey} for asset ${assetId}`);
|
||||||
await downloadFromS3(S3_BUCKET, inputKey, inputPath);
|
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
|
// 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
|
// 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
|
// when the source disconnects before any frame is received; the proxy
|
||||||
// pipeline used to bomb on "moov atom not found", which buried the
|
// 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'.
|
// 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);
|
const { size: inputBytes } = await stat(inputPath);
|
||||||
if (inputBytes < 4096) {
|
if (dbMediaType !== 'image' && inputBytes < 4096) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Source is empty or truncated (${inputBytes} bytes). The recording ` +
|
`Source is empty or truncated (${inputBytes} bytes). The recording ` +
|
||||||
`likely ended before any frames were received — check the source ` +
|
`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
|
// 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).
|
// 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.
|
// 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) {
|
if (isImage) {
|
||||||
const imageOutputKey = outputKey.replace(/\.mp4$/, '.jpg');
|
const imageOutputKey = outputKey.replace(/\.mp4$/, '.jpg');
|
||||||
const imageOutputPath = outputPath.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 job.updateProgress(40);
|
||||||
await transcodeImage(inputPath, imageOutputPath);
|
await transcodeImage(inputPath, imageOutputPath);
|
||||||
await job.updateProgress(70);
|
await job.updateProgress(70);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue