From a25e4b6071e1cd76c56faefe6387c4353d7ed1d1 Mon Sep 17 00:00:00 2001 From: ZGaetano Date: Thu, 28 May 2026 07:48:57 -0400 Subject: [PATCH] =?UTF-8?q?UXP=20v2.1.3:=20timeline.js=20=E2=80=94=20corre?= =?UTF-8?q?ct=20Premiere=20DOM=20API=20calls=20per=20official=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- services/premiere-plugin-uxp/src/timeline.js | 238 +++++++++++-------- 1 file changed, 140 insertions(+), 98 deletions(-) diff --git a/services/premiere-plugin-uxp/src/timeline.js b/services/premiere-plugin-uxp/src/timeline.js index e238907..34f9df9 100644 --- a/services/premiere-plugin-uxp/src/timeline.js +++ b/services/premiere-plugin-uxp/src/timeline.js @@ -1,12 +1,18 @@ -// timeline.js — v2.1.0 -// Reads active Premiere sequence via UXP premierepro API. -// Replaces all CSInterface.evalScript() calls that died in PPro 26. -// Also: FCP XML generation, push to MAM, conform, batch hi-res relink. +// timeline.js — v2.1.3 +// Reads active Premiere sequence via the real UXP premierepro DOM API. +// +// Key corrections from official docs (developer.adobe.com/premiere-pro/uxp/ppro-reference/): +// • Most Premiere API calls are SYNCHRONOUS — no await needed +// • Track items via VideoTrack.getTrackItems(TrackItemType.Clip, false) → VideoClipTrackItem[] +// (no getClipCount / getClip — those don't exist) +// • Media file path via ClipProjectItem.cast(projItem).getMediaFilePath() (not getMediaPath) +// • Relink via ClipProjectItem.cast(projItem).changeMediaFilePath(newPath, false) +// • TickTime.seconds is the float-seconds property (use directly, no ticks math needed) +// • project.getRootItem() and getActiveProject() are synchronous (function () { const Timeline = {}; - // ── premierepro lazy require ──────────────────────────────────── function ppro() { if (Timeline._ppro) return Timeline._ppro; try { Timeline._ppro = require('premierepro'); } @@ -16,53 +22,78 @@ // ── Read active sequence ───────────────────────────────────────── // Returns { sequenceName, frameRate, width, height, clips[] } - // clips: { fileName, filePath, trackIndex, timelineInFrames, timelineOutFrames, - // sourceInFrames, sourceOutFrames } + // clips: { fileName, filePath, trackIndex, + // timelineInSec, timelineOutSec, sourceInSec, sourceOutSec } Timeline.readActiveSequence = async function () { const P = ppro(); - const project = await P.Project.getActiveProject(); + const project = P.Project.getActiveProject(); // sync if (!project) throw new Error('No active Premiere project'); - const seq = await project.getActiveSequence(); - if (!seq) throw new Error('No active sequence in the project'); + const seq = project.getActiveSequence(); // sync + if (!seq) throw new Error('No active sequence'); - const name = await seq.getName().catch(() => 'Sequence 1'); - const settings = await seq.getSettings().catch(() => ({})); - const frameRate = settings.videoFrameRate ? (1 / settings.videoFrameRate) : 29.97; - const width = settings.videoFrameWidth || 1920; - const height = settings.videoFrameHeight || 1080; + const name = seq.name || 'Sequence 1'; + const settings = seq.getSettings(); // sync + // SequenceSettings.videoFrameRate is a FrameRate object with .seconds property + // .seconds on FrameRate = 1/fps (duration of one frame in seconds) + let frameRate = 29.97; + try { + const fr = settings.videoFrameRate; + if (fr && typeof fr.seconds === 'number' && fr.seconds > 0) { + frameRate = 1 / fr.seconds; + } + } catch (_) {} + const width = settings.videoFrameWidth || 1920; + const height = settings.videoFrameHeight || 1080; + + const clips = []; + const trackCount = seq.getVideoTrackCount(); // sync - // Walk video tracks - const clips = []; - let trackCount = 0; - try { trackCount = await seq.getVideoTrackCount(); } catch (_) {} for (let ti = 0; ti < trackCount; ti++) { - let track; - try { track = await seq.getVideoTrack(ti); } catch (_) { continue; } - let clipCount = 0; - try { clipCount = await track.getClipCount(); } catch (_) {} - for (let ci = 0; ci < clipCount; ci++) { + const track = seq.getVideoTrack(ti); // sync + if (!track) continue; + + // getTrackItems(TrackItemType, includeEmptyItems) → VideoClipTrackItem[] + let items = []; + try { + // Constants.TrackItemType.Clip = 1 + items = track.getTrackItems(1, false); + } catch (_) { continue; } + if (!items || !items.length) continue; + + for (const clip of items) { try { - const clip = await track.getClip(ci); - const projItem = await clip.getProjectItem().catch(() => null); + const projItem = clip.getProjectItem(); // sync if (!projItem) continue; - const mediaPath = await projItem.getMediaPath().catch(() => ''); - const itemName = await projItem.getName().catch(() => ''); - const inPoint = await clip.getInPoint().catch(() => ({ ticks: 0 })); - const outPoint = await clip.getOutPoint().catch(() => ({ ticks: 0 })); - const srcIn = await clip.getSourceInPoint().catch(() => ({ ticks: 0 })); - const srcOut = await clip.getSourceOutPoint().catch(() => ({ ticks: 0 })); - // Premiere ticks = 1/254016000000 s. Convert to frames. - const TICK = 254016000000; - function ticksToFrames(t) { return Math.round((t.ticks / TICK) * frameRate); } + + // Cast to ClipProjectItem to get file path + let filePath = ''; + try { + const clipItem = P.ClipProjectItem.cast(projItem); + filePath = clipItem.getMediaFilePath() || ''; // sync + } catch (_) {} + + const fileName = clip.getName() || path.basename(filePath) || 'clip'; + + // TickTime.seconds is directly available as a number property + const tlIn = clip.getStartTime().seconds; // timeline start + const tlOut = clip.getEndTime().seconds; // timeline end + const srcIn = clip.getInPoint().seconds; // source in + const srcOut= clip.getOutPoint().seconds; // source out + clips.push({ - fileName: itemName || (mediaPath ? mediaPath.split(/[\\/]/).pop() : 'clip'), - filePath: mediaPath || '', - trackIndex: ti, - timelineInFrames: ticksToFrames(inPoint), - timelineOutFrames: ticksToFrames(outPoint), - sourceInFrames: ticksToFrames(srcIn), - sourceOutFrames: ticksToFrames(srcOut), + fileName, + filePath, + trackIndex: ti, + timelineInSec: tlIn, + timelineOutSec: tlOut, + sourceInSec: srcIn, + sourceOutSec: srcOut, + // frame equivalents for FCP XML / MAM clip push + timelineInFrames: Math.round(tlIn * frameRate), + timelineOutFrames: Math.round(tlOut * frameRate), + sourceInFrames: Math.round(srcIn * frameRate), + sourceOutFrames: Math.round(srcOut * frameRate), }); } catch (_) {} } @@ -75,11 +106,11 @@ Timeline.refreshSeqBar = async function () { try { const P = ppro(); - const project = await P.Project.getActiveProject().catch(() => null); + const project = P.Project.getActiveProject(); if (!project) { UI.setHidden('#seq-info-bar', true); return; } - const seq = await project.getActiveSequence().catch(() => null); + const seq = project.getActiveSequence(); if (!seq) { UI.setHidden('#seq-info-bar', true); return; } - const name = await seq.getName().catch(() => ''); + const name = seq.name || ''; if (name) { document.getElementById('seq-info-name').textContent = name; UI.setHidden('#seq-info-bar', false); @@ -100,15 +131,13 @@ const clips = timelineData.clips || []; let totalFrames = 0; - clips.forEach(c => { if ((c.timelineOutFrames || 0) > totalFrames) totalFrames = c.timelineOutFrames; }); + clips.forEach(c => { if ((c.timelineOutFrames||0) > totalFrames) totalFrames = c.timelineOutFrames; }); if (totalFrames < 1) totalFrames = 100; - const duration = UI.timecodeFromFrames(totalFrames, frameRate); - const frateStr = UI.formatFrameRate(frameRate); + const duration = UI.timecodeFromFrames(totalFrames, frameRate); + const frateStr = UI.formatFrameRate(frameRate); - let xml = '\n'; - xml += '\n'; - xml += '\n'; + let xml = '\n\n\n'; xml += ' \n'; xml += ' \n'; @@ -118,29 +147,25 @@ if (!seen[key]) { seen[key] = 'r' + rid; const srcDur = UI.timecodeFromFrames( - Math.max(1, (clip.sourceOutFrames || 100) - (clip.sourceInFrames || 0)), frameRate + Math.max(1, (clip.sourceOutFrames||100) - (clip.sourceInFrames||0)), frameRate ); - xml += ' \n'; rid++; } }); - xml += ' \n'; - xml += ' \n \n \n'; + xml += ' \n \n \n \n'; xml += ' \n \n'; clips.forEach(clip => { const resId = seen[clip.filePath || clip.fileName || 'x'] || 'r1'; const off = UI.timecodeFromFrames(clip.timelineInFrames || 0, frameRate); - const dur = UI.timecodeFromFrames( - Math.max(1, (clip.timelineOutFrames || 1) - (clip.timelineInFrames || 0)), frameRate - ); + const dur = UI.timecodeFromFrames(Math.max(1, (clip.timelineOutFrames||1) - (clip.timelineInFrames||0)), frameRate); const srcIn = UI.timecodeFromFrames(clip.sourceInFrames || 0, frameRate); - xml += ' \n'; - xml += ' \n'; - xml += ' \n'; + xml += ' \n \n'; }); xml += ' \n \n \n \n \n'; @@ -151,9 +176,8 @@ Timeline.pushToMAM = async function (seqName, projectId, timelineData) { const resolved = Library.resolveClipsToAssets(timelineData.clips || []); const matched = resolved.filter(c => c.asset_id); - if (matched.length === 0) throw new Error('No clips matched MAM assets — import proxies first'); + if (!matched.length) throw new Error('No clips matched MAM assets — import proxies first'); - // Upsert sequence const seqs = await API.listSequences(projectId); let seqId; const existing = seqs.find(s => s.name === seqName); @@ -178,20 +202,14 @@ source_out_frames: c.sourceOutFrames, })); await API.pushClips(seqId, payload); - return { seqId, matched: matched.length, skipped: resolved.length - matched.length }; }; // ── Conform ────────────────────────────────────────────────────── - // opts: { codec, quality, resolution, audio } - // Returns jobId for polling. Timeline.startConform = async function (projectId, seqName, timelineData, opts) { - // Upsert sequence to get seqId const { seqId } = await Timeline.pushToMAM(seqName, projectId, timelineData); - const resMap = { '1080p': [1920,1080], '720p': [1280,720], 'uhd': [3840,2160], 'source': [0,0] }; const [w, h] = resMap[opts.resolution] || [1920,1080]; - const job = await API.startConform(seqId, { codec: opts.codec, quality: opts.quality, width: w || undefined, height: h || undefined, @@ -202,11 +220,10 @@ }; Timeline.pollConform = function (jobId, onProgress, onDone) { - let timer = setInterval(async () => { + const timer = setInterval(async () => { try { const job = await API.getJob(jobId); - const pct = job.progress || 0; - onProgress(pct, job.status); + onProgress(job.progress || 0, job.status); if (job.status === 'completed' || job.status === 'failed') { clearInterval(timer); onDone(job); @@ -217,11 +234,10 @@ }; // ── Batch Hi-Res Relink ────────────────────────────────────────── - // Takes selectedClips (array of resolved clip objects with asset_id), - // downloads hi-res for each, relinks in Premiere. + // selectedClips: resolved clip objects with asset_id + filePath Timeline.batchRelink = async function (selectedClips) { const P = ppro(); - const project = await P.Project.getActiveProject(); + const project = P.Project.getActiveProject(); // sync if (!project) throw new Error('No active Premiere project'); const results = { succeeded: 0, failed: 0, errors: [] }; @@ -230,19 +246,18 @@ if (!clip.asset_id) continue; try { UI.showProgress('Fetching hi-res for ' + clip.fileName + '…', 20); - const info = await API.getHiresInfo(clip.asset_id); + const info = await API.getHiresInfo(clip.asset_id); const safeName = UI.sanitizeFilename(info.filename || clip.fileName + '.' + (info.ext || 'mxf')); - const dest = await Import._tempPath(safeName); + const dest = await Import._tempPath(safeName); UI.showProgress('Downloading ' + safeName + '…', 30); const r = await API.requestFollow(info.url, {}); if (!r.ok) throw new Error('Download HTTP ' + r.status); await Import._streamToFile(r, dest, ({ received, total }) => { const pct = total ? 30 + (received / total) * 50 : 30; - UI.showProgress('Downloading ' + UI.formatBytes(received) + '…', pct); + UI.showProgress(UI.formatBytes(received) + '…', pct); }); - // Relink: walk project items, find by old path, changeMediaPath UI.showProgress('Relinking ' + clip.fileName + '…', 85); await Timeline._relinkInProject(project, clip.filePath, dest); results.succeeded++; @@ -254,31 +269,58 @@ return results; }; - // Walk all project items and changeMediaPath for items matching oldPath. + // Walk project items and changeMediaFilePath for items matching oldPath. + // Uses ClipProjectItem.cast() + changeMediaFilePath() per official docs. Timeline._relinkInProject = async function (project, oldPath, newPath) { - const root = await project.getRootItem(); - let count = 0; - async function walk(item) { + const P = ppro(); + const root = project.getRootItem(); // sync (FolderItem) + let count = 0; + + function walkSync(folderItem) { + // FolderItem has no getItems() in docs — use project.getSequences() / rootItem children + // Actually FolderItem IS a ProjectItem; children accessible via getItems on the project + // Try both known APIs gracefully + let children = []; try { - const children = await item.getItems(); - for (const child of children) { + // Some UXP builds expose children on FolderItem directly + if (typeof folderItem.getItems === 'function') { + children = folderItem.getItems() || []; + } + } catch (_) {} + + for (const item of children) { + try { + const clipItem = P.ClipProjectItem.cast(item); + const mp = clipItem.getMediaFilePath(); + if (mp && mp === oldPath) { + clipItem.changeMediaFilePath(newPath, false); + count++; + } + } catch (_) { + // cast fails for FolderItems — try to recurse + try { walkSync(item); } catch (__) {} + } + } + } + + walkSync(root); + + // If walkSync found nothing (getItems not on FolderItem in this build), + // fall back to findItemsMatchingMediaPath on root + if (count === 0) { + try { + const matches = P.ClipProjectItem.cast(root).findItemsMatchingMediaPath(oldPath, true); + for (const item of (matches || [])) { try { - const mp = await child.getMediaPath().catch(() => null); - if (mp && mp === oldPath) { - await child.changeMediaPath(newPath, true); - count++; - } - } catch (_) {} - // Recurse into bins - try { - const sub = await child.getItems(); - if (sub && sub.length) await walk(child); + const clipItem = P.ClipProjectItem.cast(item); + clipItem.changeMediaFilePath(newPath, false); + count++; } catch (_) {} } } catch (_) {} } - await walk(root); - if (count === 0) throw new Error('No matching clips found for ' + (oldPath || 'unknown path')); + + if (count === 0) throw new Error('No matching clips found for path: ' + (oldPath || 'unknown')); return count; };