feat(worker): conform — queue proxy build for the conformed output

ProRes / DNxHR conformed outputs are unplayable in the browser
(HTML5 video: MEDIA_ERR_SRC_NOT_SUPPORTED). The library was
referencing the ProRes original as the only source.

After the asset row is inserted, queue an H.264 proxy build the same
way services/mam-api/src/routes/assets.js does on ingest:
  proxyQueue.add('generate', {
    assetId,
    inputKey:  outputKey,         // the conformed mov / mp4
    outputKey: `proxies/${id}.mp4`,
  });

The proxy worker writes the H.264 mp4, updates assets.proxy_s3_key,
and from then on /assets/:id/stream prefers the proxy over the
original. The library player can decode it natively.

Failure to enqueue is logged but doesn't fail the conform job — the
asset still exists and can have a proxy re-queued later.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Claude 2026-05-28 15:49:01 -04:00
parent 446a563647
commit 6bc6478270

View file

@ -1,6 +1,7 @@
import { join } from 'path';
import { unlink, writeFile, mkdir, rm } from 'fs/promises';
import { tmpdir } from 'os';
import { Queue } from 'bullmq';
import { query } from '../db/client.js';
import { downloadFromS3, uploadToS3 } from '../s3/client.js';
import { trimSegment, concatSegments, runFFmpeg, getMediaInfo } from '../ffmpeg/executor.js';
@ -9,6 +10,19 @@ import { XMLParser } from 'fast-xml-parser';
const S3_BUCKET = process.env.S3_BUCKET || 'wild-dragon';
// Used to queue a proxy build for the conformed output so the library /
// asset viewer has a browser-playable H.264 preview. Without this the
// browser hits MEDIA_ERR_SRC_NOT_SUPPORTED on ProRes / DNxHR outputs.
const parseRedisUrl = (url) => {
try {
const parsed = new URL(url);
return { host: parsed.hostname, port: parseInt(parsed.port, 10) || 6379 };
} catch { return { host: 'localhost', port: 6379 }; }
};
const proxyQueue = new Queue('proxy', {
connection: parseRedisUrl(process.env.REDIS_URL || 'redis://queue:6379'),
});
const xmlParser = new XMLParser({
ignoreAttributes: false,
attributeNamePrefix: '@_',
@ -353,10 +367,30 @@ export const conformWorker = async (job) => {
]
);
await job.updateProgress(100);
console.log(`[conform] Job ${jobId} complete → asset ${assetRes.rows[0].id}`);
const newAssetId = assetRes.rows[0].id;
return { jobId, outputKey, assetId: assetRes.rows[0].id };
// Queue a proxy build so the library has a browser-playable H.264 file.
// ProRes / DNxHR masters don't decode in HTML5 video; without this step
// the asset shows MEDIA_ERR_SRC_NOT_SUPPORTED in the player. Mirror the
// ingest pipeline's pattern (services/mam-api/src/routes/assets.js).
try {
const generatedProxyKey = `proxies/${newAssetId}.mp4`;
await proxyQueue.add('generate', {
assetId: newAssetId,
inputKey: outputKey,
outputKey: generatedProxyKey,
});
console.log(`[conform] queued proxy build for ${newAssetId}`);
} catch (e) {
// Don't fail the conform job if the proxy queue is unreachable —
// the asset still exists, an operator can retrigger the proxy.
console.warn(`[conform] failed to queue proxy for ${newAssetId}: ${e.message}`);
}
await job.updateProgress(100);
console.log(`[conform] Job ${jobId} complete → asset ${newAssetId}`);
return { jobId, outputKey, assetId: newAssetId };
} catch (error) {
console.error(`[conform] Error in job ${jobId}:`, error);