From 0abef056e707e45050b833df5f0a75f80de68774 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 28 May 2026 13:58:13 -0400 Subject: [PATCH] =?UTF-8?q?fix(uxp+mam-api):=20Export=20Timeline=20render?= =?UTF-8?q?=20=E2=80=94=20xmeml=20schema=20+=20BullMQ=20job=20poll?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two cooperating bugs left Export Timeline stuck at "Rendering Hi-Res" forever: A. worker emitted "Invalid FCP XML: no sequence element" because Timeline.generateFcpXml produced fcpxml (FCP X schema: //...) while the worker's parseFcpXml expects xmeml (FCP 7 schema: ...). Two completely different formats. Rewrite generateFcpXml to emit xmeml v5 with the structure the parser walks: xmeml/sequence/{name,duration,rate{timebase,ntsc}, media/video/{format/samplecharacteristics, track[@currentExplodedTrackIndex] /clipitem/{name,duration,rate,in,out, start,end,file/{name,pathurl}}}} Clipitem in/out are SOURCE frames (the underlying media in/out); start/end are TIMELINE frames (the cut position). The worker uses the rate timebase to parse them. B. /api/v1/jobs/:id rejected the panel's polls with "Invalid id — must be a UUID". The handlers below correctly parse BullMQ-prefixed ids ("conform:42"), but router.param('id', validateUuid('id')) ran first and 400'd everything that wasn't a UUID. The panel's pollConform swallows the resulting fetch error silently and polls forever. Drop the validator. Comment in the file explains why. Bumps panel to v2.2.2. Co-Authored-By: Claude Opus 4.7 --- services/mam-api/src/routes/jobs.js | 7 +- services/premiere-plugin-uxp/manifest.json | 2 +- services/premiere-plugin-uxp/src/timeline.js | 107 ++++++++++++++----- 3 files changed, 84 insertions(+), 32 deletions(-) diff --git a/services/mam-api/src/routes/jobs.js b/services/mam-api/src/routes/jobs.js index 47db682..1079b19 100644 --- a/services/mam-api/src/routes/jobs.js +++ b/services/mam-api/src/routes/jobs.js @@ -1,10 +1,13 @@ import express from 'express'; import pool from '../db/pool.js'; -import { validateUuid } from '../middleware/errors.js'; import { Queue } from 'bullmq'; const router = express.Router(); -router.param('id', (req, res, next) => validateUuid('id')(req, res, next)); +// Note: jobs use BullMQ id format ":" (e.g. "conform:42"), +// NOT UUIDs. The GET/:id, POST/:id/retry, and DELETE/:id handlers below split +// on the colon themselves and look up the queue. Adding a UUID validator +// here would 400 every BullMQ poll the panel makes (which is exactly what +// caused Export Timeline to stall "Rendering Hi-Res" forever — fixed 2026-05-28). // ── Redis connection ────────────────────────────────────────────────────────── const parseRedisUrl = (url) => { diff --git a/services/premiere-plugin-uxp/manifest.json b/services/premiere-plugin-uxp/manifest.json index 245fc6f..2a3d5ff 100644 --- a/services/premiere-plugin-uxp/manifest.json +++ b/services/premiere-plugin-uxp/manifest.json @@ -2,7 +2,7 @@ "manifestVersion": 5, "id": "net.wilddragon.dragonflight.uxp", "name": "Dragonflight MAM", - "version": "2.2.1", + "version": "2.2.2", "main": "index.html", "host": { "app": "premierepro", diff --git a/services/premiere-plugin-uxp/src/timeline.js b/services/premiere-plugin-uxp/src/timeline.js index 7660bbb..c8f5e6a 100644 --- a/services/premiere-plugin-uxp/src/timeline.js +++ b/services/premiere-plugin-uxp/src/timeline.js @@ -110,40 +110,89 @@ } catch (_) { UI.setHidden('#seq-info-bar', true); } }; - // ── FCP XML ────────────────────────────────────────────────────── + // ── FCP XML (xmeml / FCP 7) ────────────────────────────────────── + // The worker's parseFcpXml expects the legacy xmeml schema + // (root ), NOT the modern fcpxml schema + // (root ...). We previously emitted + // fcpxml and every conform job failed with "Invalid FCP XML: no + // sequence element". Now we emit xmeml v5 with: + // xmeml/sequence/{name,duration,rate,media/video/{format,track*}} + // track[@currentExplodedTrackIndex] / clipitem / file{name,pathurl} + // clipitem in/out are SOURCE frames; start/end are TIMELINE frames. Timeline.generateFcpXml = function (td) { - const seqName = UI.escapeXml(td.sequenceName || 'Sequence 1'); - const fps = td.frameRate || 29.97; - const w = td.width || 1920; - const h = td.height || 1080; - const clips = td.clips || []; + const seqName = UI.escapeXml(td.sequenceName || 'Sequence 1'); + const fps = Math.round(td.frameRate || 29.97); + const w = td.width || 1920; + const h = td.height || 1080; + const clips = td.clips || []; + let totalF = 0; - clips.forEach(c => { if ((c.timelineOutFrames||0) > totalF) totalF = c.timelineOutFrames; }); + clips.forEach(c => { if ((c.timelineOutFrames || 0) > totalF) totalF = c.timelineOutFrames; }); if (totalF < 1) totalF = 100; - const dur = UI.timecodeFromFrames(totalF, fps); - const frate = UI.formatFrameRate(fps); - let xml = '\n\n\n \n'; - xml += ' \n'; - const seen = {}; let rid = 1; + + // Group clips by their video track so each contains its + // own clipitems in timeline order. The worker iterates tracks and + // collects clipitems off each. + const byTrack = {}; clips.forEach(c => { - const key = c.filePath || c.fileName || 'c' + rid; - if (!seen[key]) { - seen[key] = 'r' + rid; - const sd = UI.timecodeFromFrames(Math.max(1,(c.sourceOutFrames||100)-(c.sourceInFrames||0)), fps); - xml += ' \n'; - rid++; - } + const ti = Number(c.trackIndex || 0); + (byTrack[ti] = byTrack[ti] || []).push(c); }); - xml += ' \n \n \n \n \n \n'; - clips.forEach(c => { - const ref = seen[c.filePath || c.fileName || 'x'] || 'r1'; - const off = UI.timecodeFromFrames(c.timelineInFrames||0, fps); - const cd = UI.timecodeFromFrames(Math.max(1,(c.timelineOutFrames||1)-(c.timelineInFrames||0)), fps); - const si = UI.timecodeFromFrames(c.sourceInFrames||0, fps); - xml += ' \n \n \n'; - }); - xml += ' \n \n \n \n \n'; - return xml; + + const out = []; + out.push(''); + out.push(''); + out.push(''); + out.push(' '); + out.push(' ' + seqName + ''); + out.push(' ' + totalF + ''); + out.push(' ' + fps + 'FALSE'); + out.push(' '); + out.push(' '); + out.push(' '); + out.push(' '); + out.push(''); + return out.join('\n'); }; // ── Push Timeline to MAM ─────────────────────────────────────────