diff --git a/services/premiere-plugin-uxp/src/timeline.js b/services/premiere-plugin-uxp/src/timeline.js index 34f9df9..a27a59d 100644 --- a/services/premiere-plugin-uxp/src/timeline.js +++ b/services/premiere-plugin-uxp/src/timeline.js @@ -1,14 +1,6 @@ -// 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 +// timeline.js — v2.1.4 +// Reads active Premiere sequence via UXP premierepro DOM API. +// All Premiere DOM methods are synchronous. No redirect:manual (not in UXP). (function () { const Timeline = {}; @@ -21,27 +13,19 @@ } // ── Read active sequence ───────────────────────────────────────── - // Returns { sequenceName, frameRate, width, height, clips[] } - // clips: { fileName, filePath, trackIndex, - // timelineInSec, timelineOutSec, sourceInSec, sourceOutSec } Timeline.readActiveSequence = async function () { const P = ppro(); const project = P.Project.getActiveProject(); // sync if (!project) throw new Error('No active Premiere project'); - const seq = project.getActiveSequence(); // sync if (!seq) throw new Error('No active sequence'); 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; - } + if (fr && typeof fr.seconds === 'number' && fr.seconds > 0) frameRate = 1 / fr.seconds; } catch (_) {} const width = settings.videoFrameWidth || 1920; const height = settings.videoFrameHeight || 1080; @@ -52,44 +36,28 @@ for (let ti = 0; ti < trackCount; ti++) { 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; } + try { items = track.getTrackItems(1, false); } catch (_) { continue; } if (!items || !items.length) continue; for (const clip of items) { try { const projItem = clip.getProjectItem(); // sync if (!projItem) continue; - - // 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 - + const fileName = clip.getName() || (filePath ? path.basename(filePath) : 'clip'); + const tlIn = clip.getStartTime().seconds; + const tlOut = clip.getEndTime().seconds; + const srcIn = clip.getInPoint().seconds; + const srcOut = clip.getOutPoint().seconds; clips.push({ - fileName, - filePath, - trackIndex: ti, - timelineInSec: tlIn, - timelineOutSec: tlOut, - sourceInSec: srcIn, - sourceOutSec: srcOut, - // frame equivalents for FCP XML / MAM clip push + fileName, filePath, trackIndex: ti, + timelineInSec: tlIn, timelineOutSec: tlOut, + sourceInSec: srcIn, sourceOutSec: srcOut, timelineInFrames: Math.round(tlIn * frameRate), timelineOutFrames: Math.round(tlOut * frameRate), sourceInFrames: Math.round(srcIn * frameRate), @@ -98,7 +66,6 @@ } catch (_) {} } } - return { sequenceName: name, frameRate, width, height, clips }; }; @@ -117,57 +84,41 @@ } else { UI.setHidden('#seq-info-bar', true); } - } catch (_) { - UI.setHidden('#seq-info-bar', true); - } + } catch (_) { UI.setHidden('#seq-info-bar', true); } }; - // ── FCP XML generation ─────────────────────────────────────────── + // ── FCP XML ────────────────────────────────────────────────────── Timeline.generateFcpXml = function (timelineData) { const seqName = UI.escapeXml(timelineData.sequenceName || 'Sequence 1'); const frameRate = timelineData.frameRate || 29.97; const width = timelineData.width || 1920; const height = timelineData.height || 1080; const clips = timelineData.clips || []; - let totalFrames = 0; 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); - - let xml = '\n\n\n'; - xml += ' \n'; + let xml = '\n\n\n \n'; xml += ' \n'; - const seen = {}; let rid = 1; clips.forEach(clip => { const key = clip.filePath || clip.fileName || 'c' + rid; if (!seen[key]) { seen[key] = 'r' + rid; - const srcDur = UI.timecodeFromFrames( - Math.max(1, (clip.sourceOutFrames||100) - (clip.sourceInFrames||0)), frameRate - ); - xml += ' \n'; + const srcDur = UI.timecodeFromFrames(Math.max(1,(clip.sourceOutFrames||100)-(clip.sourceInFrames||0)), frameRate); + xml += ' \n'; rid++; } }); - xml += ' \n \n \n \n'; - xml += ' \n \n'; - + xml += ' \n \n \n \n \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 srcIn = UI.timecodeFromFrames(clip.sourceInFrames || 0, frameRate); - xml += ' \n'; - xml += ' \n \n'; + const off = UI.timecodeFromFrames(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 \n \n'; }); - xml += ' \n \n \n \n \n'; return xml; }; @@ -177,45 +128,30 @@ const resolved = Library.resolveClipsToAssets(timelineData.clips || []); const matched = resolved.filter(c => c.asset_id); if (!matched.length) throw new Error('No clips matched MAM assets — import proxies first'); - const seqs = await API.listSequences(projectId); let seqId; const existing = seqs.find(s => s.name === seqName); if (existing) { - await API.updateSequence(existing.id, { - frame_rate: timelineData.frameRate, width: timelineData.width, height: timelineData.height, - }); + await API.updateSequence(existing.id, { frame_rate: timelineData.frameRate, width: timelineData.width, height: timelineData.height }); seqId = existing.id; } else { - const created = await API.createSequence( - projectId, seqName, timelineData.frameRate, timelineData.width, timelineData.height - ); + const created = await API.createSequence(projectId, seqName, timelineData.frameRate, timelineData.width, timelineData.height); seqId = created.id; } - - const payload = matched.map(c => ({ - asset_id: c.asset_id, - track: c.trackIndex, - timeline_in_frames: c.timelineInFrames, - timeline_out_frames: c.timelineOutFrames, - source_in_frames: c.sourceInFrames, - source_out_frames: c.sourceOutFrames, - })); - await API.pushClips(seqId, payload); + await API.pushClips(seqId, matched.map(c => ({ + asset_id: c.asset_id, track: c.trackIndex, + timeline_in_frames: c.timelineInFrames, timeline_out_frames: c.timelineOutFrames, + source_in_frames: c.sourceInFrames, source_out_frames: c.sourceOutFrames, + }))); return { seqId, matched: matched.length, skipped: resolved.length - matched.length }; }; // ── Conform ────────────────────────────────────────────────────── Timeline.startConform = async function (projectId, seqName, timelineData, opts) { const { seqId } = await Timeline.pushToMAM(seqName, projectId, timelineData); - 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 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(timelineData) }); return job.jobId || job.id; }; @@ -224,22 +160,17 @@ try { const job = await API.getJob(jobId); 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(timer); onDone(job); } } catch (_) {} }, 2000); return timer; }; // ── Batch Hi-Res Relink ────────────────────────────────────────── - // selectedClips: resolved clip objects with asset_id + filePath Timeline.batchRelink = async function (selectedClips) { const P = ppro(); const project = P.Project.getActiveProject(); // sync if (!project) throw new Error('No active Premiere project'); - const results = { succeeded: 0, failed: 0, errors: [] }; for (const clip of selectedClips) { @@ -251,12 +182,13 @@ const dest = await Import._tempPath(safeName); UI.showProgress('Downloading ' + safeName + '…', 30); - const r = await API.requestFollow(info.url, {}); + // S3 presigned URL — use requestExternal (no auth header, no redirect:manual) + const r = await API.requestExternal(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(UI.formatBytes(received) + '…', pct); - }); + + UI.showProgress('Writing…', 60); + const buf = await r.arrayBuffer(); + await Import._writeBuffer(dest, buf); UI.showProgress('Relinking ' + clip.fileName + '…', 85); await Timeline._relinkInProject(project, clip.filePath, dest); @@ -269,58 +201,38 @@ return results; }; - // Walk project items and changeMediaFilePath for items matching oldPath. - // Uses ClipProjectItem.cast() + changeMediaFilePath() per official docs. + // Walk project items and relink via ClipProjectItem.changeMediaFilePath() Timeline._relinkInProject = async function (project, oldPath, newPath) { 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 { - // Some UXP builds expose children on FolderItem directly - if (typeof folderItem.getItems === 'function') { - children = folderItem.getItems() || []; - } - } catch (_) {} - + try { 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++; - } + 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 + // Fallback: findItemsMatchingMediaPath if (count === 0) { try { const matches = P.ClipProjectItem.cast(root).findItemsMatchingMediaPath(oldPath, true); for (const item of (matches || [])) { - try { - const clipItem = P.ClipProjectItem.cast(item); - clipItem.changeMediaFilePath(newPath, false); - count++; - } catch (_) {} + try { P.ClipProjectItem.cast(item).changeMediaFilePath(newPath, false); count++; } catch (_) {} } } catch (_) {} } - if (count === 0) throw new Error('No matching clips found for path: ' + (oldPath || 'unknown')); + if (count === 0) throw new Error('No matching clips found for: ' + (oldPath || 'unknown')); return count; };