dragonflight/services/premiere-plugin/jsx/premiere.jsx
ZGaetano 5bb22c17c8 feat(plugin): add exportTimelineData() and getProjectItems() to ExtendScript
exportTimelineData() walks all video tracks in the active sequence and
returns clip source/timeline frame positions + file paths so the panel JS
can map them back to MAM asset IDs for timeline export.

getProjectItems() enumerates all ProjectItems with paths — useful for
rebuilding the import mapping after a Premiere restart.
2026-05-20 00:35:18 -04:00

470 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Wild Dragon MAM - Premiere Pro ExtendScript
*
* This file runs in the Premiere Pro host context (not the browser panel).
* It is registered via <ScriptPath> in manifest.xml and called from the
* panel via csInterface.evalScript().
*
* ExtendScript is ES3-level JavaScript — no arrow functions, no const/let,
* no template literals, no destructuring.
*/
// ============================================================================
// Core Import Functions
// ============================================================================
/**
* Imports a media file into the active Premiere Pro project.
* @param {string} filePath - Full path to the file to import
* @returns {string} JSON string with success status and message
*/
function importFileToProject(filePath) {
var result = {
success: false,
message: "",
itemID: null
};
try {
if (!app.project) {
result.message = "No active Premiere Pro project";
return JSON.stringify(result);
}
var file = new File(filePath);
if (!file.exists) {
result.message = "File does not exist: " + filePath;
return JSON.stringify(result);
}
app.project.importFiles([filePath]);
result.success = true;
result.message = "File imported successfully";
return JSON.stringify(result);
} catch (error) {
result.message = "Error importing file: " + error.message;
return JSON.stringify(result);
}
}
/**
* Gets the active sequence in the project.
* @returns {string} JSON string with sequence name and ID, or nulls
*/
function getActiveSequence() {
var result = {
sequenceName: null,
sequenceID: null
};
try {
if (!app.project) return JSON.stringify(result);
var activeSequence = app.project.activeSequence;
if (activeSequence) {
result.sequenceName = activeSequence.name;
result.sequenceID = activeSequence.sequenceID;
}
return JSON.stringify(result);
} catch (error) {
return JSON.stringify(result);
}
}
/**
* Inserts a clip into the active sequence at the playhead position.
* @param {string} filePath - Full path to the media file
* @param {number} trackIndex - Video track index (1-based)
* @returns {string} JSON string with success status
*/
function insertClipToSequence(filePath, trackIndex) {
var result = {
success: false,
message: ""
};
try {
if (!app.project) {
result.message = "No active project";
return JSON.stringify(result);
}
var sequence = app.project.activeSequence;
if (!sequence) {
result.message = "No active sequence";
return JSON.stringify(result);
}
app.project.importFiles([filePath]);
var file = new File(filePath);
var fileName = file.displayName;
var rootBin = app.project.rootItem;
var projectItem = findProjectItemByName(rootBin, fileName);
if (!projectItem) {
result.message = "Could not find imported file in project";
return JSON.stringify(result);
}
if (trackIndex < 1 || trackIndex > sequence.videoTracks.numTracks) {
result.message = "Invalid track index: " + trackIndex;
return JSON.stringify(result);
}
var track = sequence.videoTracks[trackIndex - 1];
var playheadTime = sequence.getPlayerPosition();
var trackItem = track.insertClip(projectItem, playheadTime);
if (trackItem) {
result.success = true;
result.message = "Clip inserted at track " + trackIndex;
} else {
result.message = "Failed to insert clip into track";
}
return JSON.stringify(result);
} catch (error) {
result.message = "Error inserting clip: " + error.message;
return JSON.stringify(result);
}
}
/**
* Gets the current project file path.
* @returns {string} JSON string with project path and name
*/
function getProjectPath() {
var result = {
projectPath: null,
projectName: null
};
try {
if (!app.project) return JSON.stringify(result);
result.projectPath = app.project.path;
result.projectName = app.project.name;
return JSON.stringify(result);
} catch (error) {
return JSON.stringify(result);
}
}
/**
* Gets information about all video tracks in the active sequence.
* @returns {string} JSON string with an array of track objects
*/
function getSequenceTracks() {
var result = { tracks: [] };
try {
if (!app.project) return JSON.stringify(result);
var sequence = app.project.activeSequence;
if (!sequence) return JSON.stringify(result);
for (var i = 0; i < sequence.videoTracks.numTracks; i++) {
var track = sequence.videoTracks[i];
result.tracks.push({
index: i + 1,
name: track.name || ("V" + (i + 1)),
type: "video"
});
}
return JSON.stringify(result);
} catch (error) {
return JSON.stringify(result);
}
}
/**
* Gets the current playhead position in the active sequence.
* @returns {string} JSON string with timeInSeconds and SMPTE timeCode
*/
function getPlayheadPosition() {
var result = {
timeInSeconds: 0,
timeCode: ""
};
try {
if (!app.project) return JSON.stringify(result);
var sequence = app.project.activeSequence;
if (!sequence) return JSON.stringify(result);
var time = sequence.getPlayerPosition();
var ticks = parseFloat(time.ticks);
// Premiere Pro ticks: 254016000000 ticks per second
var TICKS_PER_SECOND = 254016000000;
var totalSeconds = ticks / TICKS_PER_SECOND;
result.timeInSeconds = totalSeconds;
// Build SMPTE timecode — use sequence frame rate
var frameRate = sequence.timebase ? (TICKS_PER_SECOND / parseFloat(sequence.timebase)) : 25;
var totalFrames = Math.floor(totalSeconds * frameRate);
var hours = Math.floor(totalFrames / (frameRate * 3600));
var minutes = Math.floor((totalFrames % (frameRate * 3600)) / (frameRate * 60));
var seconds = Math.floor((totalFrames % (frameRate * 60)) / frameRate);
var frames = totalFrames % Math.round(frameRate);
result.timeCode = pad(hours, 2) + ":" + pad(minutes, 2) + ":" +
pad(seconds, 2) + ":" + pad(frames, 2);
return JSON.stringify(result);
} catch (error) {
return JSON.stringify(result);
}
}
/**
* Gets basic project information.
* @returns {string} JSON string with project name, path, and sequence count
*/
function getProjectInfo() {
var result = {
projectName: "",
projectPath: "",
sequenceCount: 0
};
try {
if (!app.project) return JSON.stringify(result);
result.projectName = app.project.name;
result.projectPath = app.project.path;
var rootBin = app.project.rootItem;
if (rootBin && rootBin.children) {
for (var i = 0; i < rootBin.children.numItems; i++) {
if (rootBin.children[i].type === ProjectItemType.SEQUENCE) {
result.sequenceCount++;
}
}
}
return JSON.stringify(result);
} catch (error) {
return JSON.stringify(result);
}
}
/**
* Exports the current sequence using Adobe Media Encoder.
*
* AME must be installed. This function launches AME (if not already running)
* and queues the export job — it returns immediately; encoding happens in the
* background and the output file appears when AME finishes.
*
* @param {string} outputPath - Absolute path for the output file
* @param {string} presetPath - Absolute path to an AME preset file (.epr);
* pass an empty string to use the sequence's
* current export settings.
* @returns {string} JSON string with success flag, message, and jobId
*/
function exportSequence(outputPath, presetPath) {
var result = {
success: false,
message: "",
jobId: null
};
try {
if (!app.project) {
result.message = "No active project";
return JSON.stringify(result);
}
var sequence = app.project.activeSequence;
if (!sequence) {
result.message = "No active sequence";
return JSON.stringify(result);
}
// Ensure Adobe Media Encoder is running before queuing
app.encoder.launchEncoder();
// encodeSequence(sequence, outputFilePath, presetPath, workAreaType, removeOnCompletion)
// workAreaType: ENCODE_ENTIRE = 0, ENCODE_IN_TO_OUT = 1
var jobId = app.encoder.encodeSequence(
sequence,
outputPath,
presetPath || "",
app.encoder.ENCODE_ENTIRE,
false // keep job in AME queue after completion
);
if (jobId) {
result.success = true;
result.message = "Export queued in Adobe Media Encoder";
result.jobId = jobId;
} else {
result.message = "AME returned no job ID — verify that the preset path is valid";
}
return JSON.stringify(result);
} catch (error) {
result.message = "Export error: " + error.message;
return JSON.stringify(result);
}
}
// ============================================================================
// Timeline Export — read sequence clips for MAM round-trip
// ============================================================================
/**
* Reads all clips from all video tracks in the active sequence.
* Returns JSON with sequence settings and a flat clip array.
*
* Each clip entry contains:
* filePath absolute OS path to source media (for MAM asset ID lookup)
* fileName project panel display name
* trackIndex 0-based video track index (0 = V1, 1 = V2, …)
* sourceInFrames in-point within source media, in frames
* sourceOutFrames out-point within source media, in frames
* timelineInFrames clip start position on timeline, in frames
* timelineOutFrames clip end position on timeline, in frames
*
* sequence.timebase is ticks-per-frame. Premiere uses 254,016,000,000
* ticks/second, so fps = 254016000000 / timebase.
*/
function exportTimelineData() {
var result = {
success: false,
message: "",
sequenceName: "",
frameRate: 59.94,
width: 1920,
height: 1080,
clips: []
};
try {
if (!app.project) {
result.message = "No active project";
return JSON.stringify(result);
}
var sequence = app.project.activeSequence;
if (!sequence) {
result.message = "No active sequence";
return JSON.stringify(result);
}
result.sequenceName = sequence.name;
var TICKS_PER_SECOND = 254016000000;
var timebaseTicks = parseFloat(sequence.timebase) || 4240384;
result.frameRate = parseFloat((TICKS_PER_SECOND / timebaseTicks).toFixed(4));
try { result.width = sequence.frameSizeHorizontal || 1920; } catch (e) {}
try { result.height = sequence.frameSizeVertical || 1080; } catch (e) {}
var videoTracks = sequence.videoTracks;
for (var t = 0; t < videoTracks.numTracks; t++) {
var track = videoTracks[t];
if (!track || !track.clips) continue;
var numClips = track.clips.numItems;
for (var c = 0; c < numClips; c++) {
try {
var clip = track.clips[c];
if (!clip || !clip.projectItem) continue;
var filePath = "";
try { filePath = clip.projectItem.getMediaPath(); } catch (e) {}
var srcIn = Math.round(parseFloat(clip.inPoint.ticks) / timebaseTicks);
var srcOut = Math.round(parseFloat(clip.outPoint.ticks) / timebaseTicks);
var recIn = Math.round(parseFloat(clip.start.ticks) / timebaseTicks);
var recOut = Math.round(parseFloat(clip.end.ticks) / timebaseTicks);
// Skip degenerate clips (zero or negative duration)
if (srcOut <= srcIn || recOut <= recIn) continue;
result.clips.push({
trackIndex: t,
filePath: filePath,
fileName: clip.projectItem.name || "",
sourceInFrames: srcIn,
sourceOutFrames: srcOut,
timelineInFrames: recIn,
timelineOutFrames: recOut
});
} catch (e) { /* skip malformed clip */ }
}
}
result.success = true;
result.message = result.clips.length + " clip(s) across " + videoTracks.numTracks + " track(s)";
return JSON.stringify(result);
} catch (error) {
result.message = "Error reading timeline: " + error.message;
return JSON.stringify(result);
}
}
/**
* Returns all media ProjectItems in the current project with name + file path.
* Useful for rebuilding the asset-path lookup map after a Premiere restart
* when temp-file paths may have changed.
*/
function getProjectItems() {
var result = { items: [] };
try {
if (!app.project) return JSON.stringify(result);
_collectProjectItems(app.project.rootItem, result.items);
} catch (e) {}
return JSON.stringify(result);
}
function _collectProjectItems(bin, out) {
if (!bin || !bin.children) return;
for (var i = 0; i < bin.children.numItems; i++) {
var item = bin.children[i];
if (!item) continue;
if (item.type === ProjectItemType.BIN) {
_collectProjectItems(item, out);
} else {
var path = "";
try { path = item.getMediaPath(); } catch (e) {}
out.push({ name: item.name, path: path });
}
}
}
// ============================================================================
// Helper Functions
// ============================================================================
/**
* Recursively searches for a project item by display name.
* @param {ProjectItem} bin - The bin/root to search
* @param {string} name - Name to match
* @returns {ProjectItem|null}
*/
function findProjectItemByName(bin, name) {
if (!bin || !bin.children) return null;
for (var i = 0; i < bin.children.numItems; i++) {
var item = bin.children[i];
if (item.name === name) return item;
if (item.type === ProjectItemType.BIN) {
var found = findProjectItemByName(item, name);
if (found) return found;
}
}
return null;
}
/**
* Pads a number with leading zeros.
* @param {number} num - Number to pad
* @param {number} digits - Minimum digit count
* @returns {string}
*/
function pad(num, digits) {
var str = "" + Math.floor(num);
while (str.length < digits) str = "0" + str;
return str;
}