From 60e5093c6bd9db55d8d0d1e8b0205d5b9df46232 Mon Sep 17 00:00:00 2001 From: ZGaetano Date: Thu, 28 May 2026 11:12:32 -0400 Subject: [PATCH] fix(uxp): null-safe Time object access in readActiveSequence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getStartTime/getEndTime/getInPoint/getOutPoint can return null for non-clip track items (gaps, transitions) that slip past the getProjectItem check. Accessing .seconds on null threw a TypeError that the outer catch swallowed — silently dropping every clip and leaving clips[] empty, so the export panel never opened. Also skip clips where all four time values resolve to 0 (filler items). --- services/premiere-plugin-uxp/src/timeline.js | 51 ++++++++++++++------ 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/services/premiere-plugin-uxp/src/timeline.js b/services/premiere-plugin-uxp/src/timeline.js index f045feb..7660bbb 100644 --- a/services/premiere-plugin-uxp/src/timeline.js +++ b/services/premiere-plugin-uxp/src/timeline.js @@ -1,4 +1,4 @@ -// timeline.js — v2.1.5 +// timeline.js — v2.1.6 // premierepro API: docs say sync, runtime returns Promises. Await everything. (function () { @@ -11,23 +11,31 @@ return Timeline._ppro; } + // Safe helper — returns the numeric seconds value from a Time object, + // or 0 if the object is null/undefined/malformed. + function _secs(t) { + if (!t) return 0; + if (typeof t.seconds === 'number') return t.seconds; + return 0; + } + // ── Read active sequence ───────────────────────────────────────── Timeline.readActiveSequence = async function () { const P = ppro(); const project = await P.Project.getActiveProject(); if (!project) throw new Error('No active Premiere project'); const seq = await project.getActiveSequence(); - if (!seq) throw new Error('No active sequence'); + if (!seq) throw new Error('No active sequence — open a sequence in the timeline first'); - const name = seq.name || (await seq.getName && await seq.getName()) || 'Sequence 1'; + const name = seq.name || (seq.getName ? await seq.getName() : '') || 'Sequence 1'; const settings = await seq.getSettings(); let frameRate = 29.97; try { - const fr = settings.videoFrameRate; + const fr = settings && 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 width = (settings && settings.videoFrameWidth) || 1920; + const height = (settings && settings.videoFrameHeight) || 1080; const clips = []; const trackCount = await seq.getVideoTrackCount(); @@ -43,21 +51,30 @@ try { const projItem = await clip.getProjectItem(); if (!projItem) continue; + let filePath = ''; try { const clipItem = await P.ClipProjectItem.cast(projItem); filePath = await clipItem.getMediaFilePath() || ''; } catch (_) {} + const clipName = await clip.getName().catch(() => ''); const fileName = clipName || (filePath ? path.basename(filePath) : 'clip'); - const startT = await clip.getStartTime(); - const endT = await clip.getEndTime(); - 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; + + // Null-safe time access — non-clip items can return null Time objects + const startT = await clip.getStartTime().catch(() => null); + const endT = await clip.getEndTime().catch(() => null); + const inT = await clip.getInPoint().catch(() => null); + const outT = await clip.getOutPoint().catch(() => null); + + const tlIn = _secs(startT); + const tlOut = _secs(endT); + const srcIn = _secs(inT); + const srcOut = _secs(outT); + + // Skip filler/gap items where all times are zero + if (tlIn === 0 && tlOut === 0 && srcIn === 0 && srcOut === 0 && !filePath) continue; + clips.push({ fileName, filePath, trackIndex: ti, timelineInSec: tlIn, timelineOutSec: tlOut, @@ -67,7 +84,9 @@ sourceInFrames: Math.round(srcIn * frameRate), sourceOutFrames: Math.round(srcOut * frameRate), }); - } catch (_) {} + } catch (clipErr) { + console.warn('[df] readActiveSequence: skipped clip on track', ti, '—', clipErr && clipErr.message); + } } } return { sequenceName: name, frameRate, width, height, clips }; @@ -131,7 +150,7 @@ Timeline.pushToMAM = async function (seqName, projectId, td) { const resolved = Library.resolveClipsToAssets(td.clips || []); 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 so the plugin can map file paths to asset IDs'); const seqs = await API.listSequences(projectId); let seqId; const existing = seqs.find(s => s.name === seqName);