UXP v2.1.5: timeline.js — await all premierepro calls; runtime is async
This commit is contained in:
parent
1fb790a569
commit
5774f61ac7
1 changed files with 77 additions and 77 deletions
|
|
@ -1,6 +1,5 @@
|
||||||
// timeline.js — v2.1.4
|
// timeline.js — v2.1.5
|
||||||
// Reads active Premiere sequence via UXP premierepro DOM API.
|
// premierepro API: docs say sync, runtime returns Promises. Await everything.
|
||||||
// All Premiere DOM methods are synchronous. No redirect:manual (not in UXP).
|
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
const Timeline = {};
|
const Timeline = {};
|
||||||
|
|
@ -15,13 +14,13 @@
|
||||||
// ── Read active sequence ─────────────────────────────────────────
|
// ── Read active sequence ─────────────────────────────────────────
|
||||||
Timeline.readActiveSequence = async function () {
|
Timeline.readActiveSequence = async function () {
|
||||||
const P = ppro();
|
const P = ppro();
|
||||||
const project = P.Project.getActiveProject(); // sync
|
const project = await P.Project.getActiveProject();
|
||||||
if (!project) throw new Error('No active Premiere project');
|
if (!project) throw new Error('No active Premiere project');
|
||||||
const seq = project.getActiveSequence(); // sync
|
const seq = await project.getActiveSequence();
|
||||||
if (!seq) throw new Error('No active sequence');
|
if (!seq) throw new Error('No active sequence');
|
||||||
|
|
||||||
const name = seq.name || 'Sequence 1';
|
const name = seq.name || (await seq.getName && await seq.getName()) || 'Sequence 1';
|
||||||
const settings = seq.getSettings(); // sync
|
const settings = await seq.getSettings();
|
||||||
let frameRate = 29.97;
|
let frameRate = 29.97;
|
||||||
try {
|
try {
|
||||||
const fr = settings.videoFrameRate;
|
const fr = settings.videoFrameRate;
|
||||||
|
|
@ -31,29 +30,34 @@
|
||||||
const height = settings.videoFrameHeight || 1080;
|
const height = settings.videoFrameHeight || 1080;
|
||||||
|
|
||||||
const clips = [];
|
const clips = [];
|
||||||
const trackCount = seq.getVideoTrackCount(); // sync
|
const trackCount = await seq.getVideoTrackCount();
|
||||||
|
|
||||||
for (let ti = 0; ti < trackCount; ti++) {
|
for (let ti = 0; ti < trackCount; ti++) {
|
||||||
const track = seq.getVideoTrack(ti); // sync
|
const track = await seq.getVideoTrack(ti);
|
||||||
if (!track) continue;
|
if (!track) continue;
|
||||||
let items = [];
|
let items = [];
|
||||||
try { items = track.getTrackItems(1, false); } catch (_) { continue; }
|
try { items = await track.getTrackItems(1, false); } catch (_) { continue; }
|
||||||
if (!items || !items.length) continue;
|
if (!items || !items.length) continue;
|
||||||
|
|
||||||
for (const clip of items) {
|
for (const clip of items) {
|
||||||
try {
|
try {
|
||||||
const projItem = clip.getProjectItem(); // sync
|
const projItem = await clip.getProjectItem();
|
||||||
if (!projItem) continue;
|
if (!projItem) continue;
|
||||||
let filePath = '';
|
let filePath = '';
|
||||||
try {
|
try {
|
||||||
const clipItem = P.ClipProjectItem.cast(projItem);
|
const clipItem = await P.ClipProjectItem.cast(projItem);
|
||||||
filePath = clipItem.getMediaFilePath() || ''; // sync
|
filePath = await clipItem.getMediaFilePath() || '';
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
const fileName = clip.getName() || (filePath ? path.basename(filePath) : 'clip');
|
const clipName = await clip.getName().catch(() => '');
|
||||||
const tlIn = clip.getStartTime().seconds;
|
const fileName = clipName || (filePath ? path.basename(filePath) : 'clip');
|
||||||
const tlOut = clip.getEndTime().seconds;
|
const startT = await clip.getStartTime();
|
||||||
const srcIn = clip.getInPoint().seconds;
|
const endT = await clip.getEndTime();
|
||||||
const srcOut = clip.getOutPoint().seconds;
|
const inT = await clip.getInPoint();
|
||||||
|
const outT = await clip.getOutPoint();
|
||||||
|
const tlIn = startT.seconds;
|
||||||
|
const tlOut = endT.seconds;
|
||||||
|
const srcIn = inT.seconds;
|
||||||
|
const srcOut = outT.seconds;
|
||||||
clips.push({
|
clips.push({
|
||||||
fileName, filePath, trackIndex: ti,
|
fileName, filePath, trackIndex: ti,
|
||||||
timelineInSec: tlIn, timelineOutSec: tlOut,
|
timelineInSec: tlIn, timelineOutSec: tlOut,
|
||||||
|
|
@ -69,13 +73,13 @@
|
||||||
return { sequenceName: name, frameRate, width, height, clips };
|
return { sequenceName: name, frameRate, width, height, clips };
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Active sequence info bar ─────────────────────────────────────
|
// ── Sequence info bar ────────────────────────────────────────────
|
||||||
Timeline.refreshSeqBar = async function () {
|
Timeline.refreshSeqBar = async function () {
|
||||||
try {
|
try {
|
||||||
const P = ppro();
|
const P = ppro();
|
||||||
const project = P.Project.getActiveProject();
|
const project = await P.Project.getActiveProject();
|
||||||
if (!project) { UI.setHidden('#seq-info-bar', true); return; }
|
if (!project) { UI.setHidden('#seq-info-bar', true); return; }
|
||||||
const seq = project.getActiveSequence();
|
const seq = await project.getActiveSequence();
|
||||||
if (!seq) { UI.setHidden('#seq-info-bar', true); return; }
|
if (!seq) { UI.setHidden('#seq-info-bar', true); return; }
|
||||||
const name = seq.name || '';
|
const name = seq.name || '';
|
||||||
if (name) {
|
if (name) {
|
||||||
|
|
@ -88,54 +92,54 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── FCP XML ──────────────────────────────────────────────────────
|
// ── FCP XML ──────────────────────────────────────────────────────
|
||||||
Timeline.generateFcpXml = function (timelineData) {
|
Timeline.generateFcpXml = function (td) {
|
||||||
const seqName = UI.escapeXml(timelineData.sequenceName || 'Sequence 1');
|
const seqName = UI.escapeXml(td.sequenceName || 'Sequence 1');
|
||||||
const frameRate = timelineData.frameRate || 29.97;
|
const fps = td.frameRate || 29.97;
|
||||||
const width = timelineData.width || 1920;
|
const w = td.width || 1920;
|
||||||
const height = timelineData.height || 1080;
|
const h = td.height || 1080;
|
||||||
const clips = timelineData.clips || [];
|
const clips = td.clips || [];
|
||||||
let totalFrames = 0;
|
let totalF = 0;
|
||||||
clips.forEach(c => { if ((c.timelineOutFrames||0) > totalFrames) totalFrames = c.timelineOutFrames; });
|
clips.forEach(c => { if ((c.timelineOutFrames||0) > totalF) totalF = c.timelineOutFrames; });
|
||||||
if (totalFrames < 1) totalFrames = 100;
|
if (totalF < 1) totalF = 100;
|
||||||
const duration = UI.timecodeFromFrames(totalFrames, frameRate);
|
const dur = UI.timecodeFromFrames(totalF, fps);
|
||||||
const frateStr = UI.formatFrameRate(frameRate);
|
const frate = UI.formatFrameRate(fps);
|
||||||
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE fcpxml>\n<fcpxml version="1.10">\n <resources>\n';
|
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE fcpxml>\n<fcpxml version="1.10">\n <resources>\n';
|
||||||
xml += ' <format id="r0" frameDuration="' + frateStr + '" width="' + width + '" height="' + height + '"/>\n';
|
xml += ' <format id="r0" frameDuration="' + frate + '" width="' + w + '" height="' + h + '"/>\n';
|
||||||
const seen = {}; let rid = 1;
|
const seen = {}; let rid = 1;
|
||||||
clips.forEach(clip => {
|
clips.forEach(c => {
|
||||||
const key = clip.filePath || clip.fileName || 'c' + rid;
|
const key = c.filePath || c.fileName || 'c' + rid;
|
||||||
if (!seen[key]) {
|
if (!seen[key]) {
|
||||||
seen[key] = 'r' + rid;
|
seen[key] = 'r' + rid;
|
||||||
const srcDur = UI.timecodeFromFrames(Math.max(1,(clip.sourceOutFrames||100)-(clip.sourceInFrames||0)), frameRate);
|
const sd = UI.timecodeFromFrames(Math.max(1,(c.sourceOutFrames||100)-(c.sourceInFrames||0)), fps);
|
||||||
xml += ' <asset id="r' + rid + '" name="' + UI.escapeXml(clip.fileName||'Clip') + '" src="' + UI.escapeXml(clip.filePath||'') + '" duration="' + srcDur + '" start="0s" format="r0"/>\n';
|
xml += ' <asset id="r' + rid + '" name="' + UI.escapeXml(c.fileName||'Clip') + '" src="' + UI.escapeXml(c.filePath||'') + '" duration="' + sd + '" start="0s" format="r0"/>\n';
|
||||||
rid++;
|
rid++;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
xml += ' </resources>\n <library>\n <event name="Conform Export">\n <project name="' + seqName + '">\n <sequence duration="' + duration + '" format="r0">\n <spine>\n';
|
xml += ' </resources>\n <library>\n <event name="Conform Export">\n <project name="' + seqName + '">\n <sequence duration="' + dur + '" format="r0">\n <spine>\n';
|
||||||
clips.forEach(clip => {
|
clips.forEach(c => {
|
||||||
const resId = seen[clip.filePath || clip.fileName || 'x'] || 'r1';
|
const ref = seen[c.filePath || c.fileName || 'x'] || 'r1';
|
||||||
const off = UI.timecodeFromFrames(clip.timelineInFrames||0, frameRate);
|
const off = UI.timecodeFromFrames(c.timelineInFrames||0, fps);
|
||||||
const dur = UI.timecodeFromFrames(Math.max(1,(clip.timelineOutFrames||1)-(clip.timelineInFrames||0)), frameRate);
|
const cd = UI.timecodeFromFrames(Math.max(1,(c.timelineOutFrames||1)-(c.timelineInFrames||0)), fps);
|
||||||
const srcIn = UI.timecodeFromFrames(clip.sourceInFrames||0, frameRate);
|
const si = UI.timecodeFromFrames(c.sourceInFrames||0, fps);
|
||||||
xml += ' <clip name="' + UI.escapeXml(clip.fileName||'Clip') + '" offset="' + off + '" duration="' + dur + '" start="' + srcIn + '">\n <asset-clip ref="' + resId + '"/>\n </clip>\n';
|
xml += ' <clip name="' + UI.escapeXml(c.fileName||'Clip') + '" offset="' + off + '" duration="' + cd + '" start="' + si + '">\n <asset-clip ref="' + ref + '"/>\n </clip>\n';
|
||||||
});
|
});
|
||||||
xml += ' </spine>\n </sequence>\n </project>\n </event>\n </library>\n</fcpxml>';
|
xml += ' </spine>\n </sequence>\n </project>\n </event>\n </library>\n</fcpxml>';
|
||||||
return xml;
|
return xml;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Push Timeline to MAM ─────────────────────────────────────────
|
// ── Push Timeline to MAM ─────────────────────────────────────────
|
||||||
Timeline.pushToMAM = async function (seqName, projectId, timelineData) {
|
Timeline.pushToMAM = async function (seqName, projectId, td) {
|
||||||
const resolved = Library.resolveClipsToAssets(timelineData.clips || []);
|
const resolved = Library.resolveClipsToAssets(td.clips || []);
|
||||||
const matched = resolved.filter(c => c.asset_id);
|
const matched = resolved.filter(c => c.asset_id);
|
||||||
if (!matched.length) 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');
|
||||||
const seqs = await API.listSequences(projectId);
|
const seqs = await API.listSequences(projectId);
|
||||||
let seqId;
|
let seqId;
|
||||||
const existing = seqs.find(s => s.name === seqName);
|
const existing = seqs.find(s => s.name === seqName);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
await API.updateSequence(existing.id, { frame_rate: timelineData.frameRate, width: timelineData.width, height: timelineData.height });
|
await API.updateSequence(existing.id, { frame_rate: td.frameRate, width: td.width, height: td.height });
|
||||||
seqId = existing.id;
|
seqId = existing.id;
|
||||||
} else {
|
} else {
|
||||||
const created = await API.createSequence(projectId, seqName, timelineData.frameRate, timelineData.width, timelineData.height);
|
const created = await API.createSequence(projectId, seqName, td.frameRate, td.width, td.height);
|
||||||
seqId = created.id;
|
seqId = created.id;
|
||||||
}
|
}
|
||||||
await API.pushClips(seqId, matched.map(c => ({
|
await API.pushClips(seqId, matched.map(c => ({
|
||||||
|
|
@ -147,49 +151,43 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Conform ──────────────────────────────────────────────────────
|
// ── Conform ──────────────────────────────────────────────────────
|
||||||
Timeline.startConform = async function (projectId, seqName, timelineData, opts) {
|
Timeline.startConform = async function (projectId, seqName, td, opts) {
|
||||||
const { seqId } = await Timeline.pushToMAM(seqName, projectId, timelineData);
|
const { seqId } = await Timeline.pushToMAM(seqName, projectId, td);
|
||||||
const resMap = { '1080p':[1920,1080], '720p':[1280,720], 'uhd':[3840,2160], 'source':[0,0] };
|
const resMap = { '1080p':[1920,1080], '720p':[1280,720], 'uhd':[3840,2160], 'source':[0,0] };
|
||||||
const [w, h] = resMap[opts.resolution] || [1920,1080];
|
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, audio: opts.audio, fcp_xml: Timeline.generateFcpXml(timelineData) });
|
const job = await API.startConform(seqId, { codec:opts.codec, quality:opts.quality, width:w||undefined, height:h||undefined, audio:opts.audio, fcp_xml:Timeline.generateFcpXml(td) });
|
||||||
return job.jobId || job.id;
|
return job.jobId || job.id;
|
||||||
};
|
};
|
||||||
|
|
||||||
Timeline.pollConform = function (jobId, onProgress, onDone) {
|
Timeline.pollConform = function (jobId, onProgress, onDone) {
|
||||||
const timer = setInterval(async () => {
|
const t = setInterval(async () => {
|
||||||
try {
|
try {
|
||||||
const job = await API.getJob(jobId);
|
const job = await API.getJob(jobId);
|
||||||
onProgress(job.progress || 0, job.status);
|
onProgress(job.progress||0, job.status);
|
||||||
if (job.status === 'completed' || job.status === 'failed') { clearInterval(timer); onDone(job); }
|
if (job.status==='completed'||job.status==='failed') { clearInterval(t); onDone(job); }
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}, 2000);
|
}, 2000);
|
||||||
return timer;
|
return t;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Batch Hi-Res Relink ──────────────────────────────────────────
|
// ── Batch Hi-Res Relink ──────────────────────────────────────────
|
||||||
Timeline.batchRelink = async function (selectedClips) {
|
Timeline.batchRelink = async function (selectedClips) {
|
||||||
const P = ppro();
|
const P = ppro();
|
||||||
const project = P.Project.getActiveProject(); // sync
|
const project = await P.Project.getActiveProject();
|
||||||
if (!project) throw new Error('No active Premiere project');
|
if (!project) throw new Error('No active Premiere project');
|
||||||
const results = { succeeded: 0, failed: 0, errors: [] };
|
const results = { succeeded: 0, failed: 0, errors: [] };
|
||||||
|
|
||||||
for (const clip of selectedClips) {
|
for (const clip of selectedClips) {
|
||||||
if (!clip.asset_id) continue;
|
if (!clip.asset_id) continue;
|
||||||
try {
|
try {
|
||||||
UI.showProgress('Fetching hi-res for ' + clip.fileName + '…', 20);
|
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 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);
|
UI.showProgress('Downloading ' + safeName + '…', 30);
|
||||||
// S3 presigned URL — use requestExternal (no auth header, no redirect:manual)
|
|
||||||
const r = await API.requestExternal(info.url);
|
const r = await API.requestExternal(info.url);
|
||||||
if (!r.ok) throw new Error('Download HTTP ' + r.status);
|
if (!r.ok) throw new Error('Download HTTP ' + r.status);
|
||||||
|
|
||||||
UI.showProgress('Writing…', 60);
|
|
||||||
const buf = await r.arrayBuffer();
|
const buf = await r.arrayBuffer();
|
||||||
await Import._writeBuffer(dest, buf);
|
await Import._writeBuffer(dest, buf);
|
||||||
|
|
||||||
UI.showProgress('Relinking ' + clip.fileName + '…', 85);
|
UI.showProgress('Relinking ' + clip.fileName + '…', 85);
|
||||||
await Timeline._relinkInProject(project, clip.filePath, dest);
|
await Timeline._relinkInProject(project, clip.filePath, dest);
|
||||||
results.succeeded++;
|
results.succeeded++;
|
||||||
|
|
@ -201,38 +199,40 @@
|
||||||
return results;
|
return results;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Walk project items and relink via ClipProjectItem.changeMediaFilePath()
|
// Relink via ClipProjectItem.changeMediaFilePath — await all calls
|
||||||
Timeline._relinkInProject = async function (project, oldPath, newPath) {
|
Timeline._relinkInProject = async function (project, oldPath, newPath) {
|
||||||
const P = ppro();
|
const P = ppro();
|
||||||
const root = project.getRootItem(); // sync (FolderItem)
|
const root = await project.getRootItem();
|
||||||
let count = 0;
|
let count = 0;
|
||||||
|
|
||||||
function walkSync(folderItem) {
|
async function walk(item) {
|
||||||
let children = [];
|
let children = [];
|
||||||
try { if (typeof folderItem.getItems === 'function') children = folderItem.getItems() || []; } catch (_) {}
|
try { children = await item.getItems(); } catch (_) {}
|
||||||
for (const item of children) {
|
for (const child of children) {
|
||||||
try {
|
try {
|
||||||
const clipItem = P.ClipProjectItem.cast(item);
|
const ci = await P.ClipProjectItem.cast(child);
|
||||||
const mp = clipItem.getMediaFilePath();
|
const mp = await ci.getMediaFilePath();
|
||||||
if (mp && mp === oldPath) { clipItem.changeMediaFilePath(newPath, false); count++; }
|
if (mp && mp === oldPath) { await ci.changeMediaFilePath(newPath, false); count++; }
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
try { walkSync(item); } catch (__) {}
|
try { await walk(child); } catch (__) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
walkSync(root);
|
|
||||||
|
await walk(root);
|
||||||
|
|
||||||
// Fallback: findItemsMatchingMediaPath
|
// Fallback: findItemsMatchingMediaPath
|
||||||
if (count === 0) {
|
if (count === 0) {
|
||||||
try {
|
try {
|
||||||
const matches = P.ClipProjectItem.cast(root).findItemsMatchingMediaPath(oldPath, true);
|
const rootCi = await P.ClipProjectItem.cast(root);
|
||||||
for (const item of (matches || [])) {
|
const matches = await rootCi.findItemsMatchingMediaPath(oldPath, true);
|
||||||
try { P.ClipProjectItem.cast(item).changeMediaFilePath(newPath, false); count++; } catch (_) {}
|
for (const item of (matches||[])) {
|
||||||
|
try { const ci = await P.ClipProjectItem.cast(item); await ci.changeMediaFilePath(newPath, false); count++; } catch (_) {}
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (count === 0) throw new Error('No matching clips found for: ' + (oldPath || 'unknown'));
|
if (count === 0) throw new Error('No matching clips found for: ' + (oldPath||'unknown'));
|
||||||
return count;
|
return count;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue