fix(premiere-plugin): CSInterface init, correct API prefix, Node.js download, lazy thumbnails, proper ExtendScript export API: premiere.jsx

This commit is contained in:
Zac Gaetano 2026-05-15 21:36:15 -04:00
parent a239e30ef2
commit 668e7c6c24

View file

@ -1,11 +1,12 @@
/** /**
* Wild Dragon MAM - Premiere Pro ExtendScript * Wild Dragon MAM - Premiere Pro ExtendScript
* *
* This file contains ExtendScript functions that interact with Premiere Pro * This file runs in the Premiere Pro host context (not the browser panel).
* to import media files and manage the timeline. * It is registered via <ScriptPath> in manifest.xml and called from the
* panel via csInterface.evalScript().
* *
* ExtendScript is Adobe's implementation of JavaScript that runs in the * ExtendScript is ES3-level JavaScript no arrow functions, no const/let,
* Premiere Pro host application context. * no template literals, no destructuring.
*/ */
// ============================================================================ // ============================================================================
@ -13,7 +14,7 @@
// ============================================================================ // ============================================================================
/** /**
* Imports a media file into the active Premiere Pro project * Imports a media file into the active Premiere Pro project.
* @param {string} filePath - Full path to the file to import * @param {string} filePath - Full path to the file to import
* @returns {string} JSON string with success status and message * @returns {string} JSON string with success status and message
*/ */
@ -25,25 +26,21 @@ function importFileToProject(filePath) {
}; };
try { try {
// Check if project is open
if (!app.project) { if (!app.project) {
result.message = "No active Premiere Pro project"; result.message = "No active Premiere Pro project";
return JSON.stringify(result); return JSON.stringify(result);
} }
// Check if file exists
var file = new File(filePath); var file = new File(filePath);
if (!file.exists) { if (!file.exists) {
result.message = "File does not exist: " + filePath; result.message = "File does not exist: " + filePath;
return JSON.stringify(result); return JSON.stringify(result);
} }
// Import the file into the project
app.project.importFiles([filePath]); app.project.importFiles([filePath]);
result.success = true; result.success = true;
result.message = "File imported successfully"; result.message = "File imported successfully";
return JSON.stringify(result); return JSON.stringify(result);
} catch (error) { } catch (error) {
result.message = "Error importing file: " + error.message; result.message = "Error importing file: " + error.message;
@ -52,8 +49,8 @@ function importFileToProject(filePath) {
} }
/** /**
* Gets the active sequence in the project * Gets the active sequence in the project.
* @returns {string} JSON string with sequence name or null * @returns {string} JSON string with sequence name and ID, or nulls
*/ */
function getActiveSequence() { function getActiveSequence() {
var result = { var result = {
@ -62,16 +59,13 @@ function getActiveSequence() {
}; };
try { try {
if (!app.project) { if (!app.project) return JSON.stringify(result);
return JSON.stringify(result);
}
var activeSequence = app.project.activeSequence; var activeSequence = app.project.activeSequence;
if (activeSequence) { if (activeSequence) {
result.sequenceName = activeSequence.name; result.sequenceName = activeSequence.name;
result.sequenceID = activeSequence.sequenceID; result.sequenceID = activeSequence.sequenceID;
} }
return JSON.stringify(result); return JSON.stringify(result);
} catch (error) { } catch (error) {
return JSON.stringify(result); return JSON.stringify(result);
@ -79,8 +73,8 @@ function getActiveSequence() {
} }
/** /**
* Inserts a clip into the active sequence at the playhead position * Inserts a clip into the active sequence at the playhead position.
* @param {string} filePath - Full path to the media file * @param {string} filePath - Full path to the media file
* @param {number} trackIndex - Video track index (1-based) * @param {number} trackIndex - Video track index (1-based)
* @returns {string} JSON string with success status * @returns {string} JSON string with success status
*/ */
@ -102,42 +96,25 @@ function insertClipToSequence(filePath, trackIndex) {
return JSON.stringify(result); return JSON.stringify(result);
} }
// Import the file
app.project.importFiles([filePath]); app.project.importFiles([filePath]);
// Find the imported item in the project
var projectItem = null;
var file = new File(filePath); var file = new File(filePath);
var fileName = file.displayName; var fileName = file.displayName;
// Search through project items for the imported file
var rootBin = app.project.rootItem; var rootBin = app.project.rootItem;
projectItem = findProjectItemByName(rootBin, fileName); var projectItem = findProjectItemByName(rootBin, fileName);
if (!projectItem) { if (!projectItem) {
result.message = "Could not find imported file in project"; result.message = "Could not find imported file in project";
return JSON.stringify(result); return JSON.stringify(result);
} }
// Get the track at the specified index
if (trackIndex < 1 || trackIndex > sequence.videoTracks.numTracks) { if (trackIndex < 1 || trackIndex > sequence.videoTracks.numTracks) {
result.message = "Invalid track index: " + trackIndex; result.message = "Invalid track index: " + trackIndex;
return JSON.stringify(result); return JSON.stringify(result);
} }
var track = sequence.videoTracks[trackIndex - 1]; var track = sequence.videoTracks[trackIndex - 1];
var playheadTime = sequence.getTime(); var playheadTime = sequence.getPlayerPosition();
// Create a clip from the project item
var clip = projectItem.getMedia();
if (!clip) {
result.message = "Could not get media from project item";
return JSON.stringify(result);
}
// Insert at playhead position
// Note: This is a simplified version. Real timeline editing requires
// more sophisticated handling of clips and tracks
var trackItem = track.insertClip(projectItem, playheadTime); var trackItem = track.insertClip(projectItem, playheadTime);
if (trackItem) { if (trackItem) {
@ -155,8 +132,8 @@ function insertClipToSequence(filePath, trackIndex) {
} }
/** /**
* Gets the current project file path * Gets the current project file path.
* @returns {string} JSON string with project path * @returns {string} JSON string with project path and name
*/ */
function getProjectPath() { function getProjectPath() {
var result = { var result = {
@ -165,16 +142,10 @@ function getProjectPath() {
}; };
try { try {
if (!app.project) { if (!app.project) return JSON.stringify(result);
return JSON.stringify(result);
}
var projectFile = app.project.path;
if (projectFile && projectFile.exists) {
result.projectPath = projectFile.fsName;
result.projectName = projectFile.displayName;
}
result.projectPath = app.project.path;
result.projectName = app.project.name;
return JSON.stringify(result); return JSON.stringify(result);
} catch (error) { } catch (error) {
return JSON.stringify(result); return JSON.stringify(result);
@ -182,33 +153,26 @@ function getProjectPath() {
} }
/** /**
* Gets information about all video tracks in the active sequence * Gets information about all video tracks in the active sequence.
* @returns {string} JSON string with track information * @returns {string} JSON string with an array of track objects
*/ */
function getSequenceTracks() { function getSequenceTracks() {
var result = { var result = { tracks: [] };
tracks: []
};
try { try {
if (!app.project) { if (!app.project) return JSON.stringify(result);
return JSON.stringify(result);
}
var sequence = app.project.activeSequence; var sequence = app.project.activeSequence;
if (!sequence) { if (!sequence) return JSON.stringify(result);
return JSON.stringify(result);
}
for (var i = 0; i < sequence.videoTracks.numTracks; i++) { for (var i = 0; i < sequence.videoTracks.numTracks; i++) {
var track = sequence.videoTracks[i]; var track = sequence.videoTracks[i];
result.tracks.push({ result.tracks.push({
index: i + 1, index: i + 1,
name: track.name || ("V" + (i + 1)), name: track.name || ("V" + (i + 1)),
type: "video" type: "video"
}); });
} }
return JSON.stringify(result); return JSON.stringify(result);
} catch (error) { } catch (error) {
return JSON.stringify(result); return JSON.stringify(result);
@ -216,8 +180,8 @@ function getSequenceTracks() {
} }
/** /**
* Gets the current playhead position in the active sequence * Gets the current playhead position in the active sequence.
* @returns {string} JSON string with playhead time in seconds * @returns {string} JSON string with timeInSeconds and SMPTE timeCode
*/ */
function getPlayheadPosition() { function getPlayheadPosition() {
var result = { var result = {
@ -226,128 +190,59 @@ function getPlayheadPosition() {
}; };
try { try {
if (!app.project) { if (!app.project) return JSON.stringify(result);
return JSON.stringify(result);
}
var sequence = app.project.activeSequence; var sequence = app.project.activeSequence;
if (!sequence) { if (!sequence) return JSON.stringify(result);
return JSON.stringify(result);
}
var time = sequence.getTime(); var time = sequence.getPlayerPosition();
result.timeInSeconds = time.seconds; 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;
// Format as timecode // Build SMPTE timecode use sequence frame rate
var ticks = time.ticks; var frameRate = sequence.timebase ? (TICKS_PER_SECOND / parseFloat(sequence.timebase)) : 25;
var ticksPerFrame = sequence.timebase; var totalFrames = Math.floor(totalSeconds * frameRate);
var frameNumber = Math.floor(ticks / ticksPerFrame); var hours = Math.floor(totalFrames / (frameRate * 3600));
var fps = sequence.timebase / 254016000000; var minutes = Math.floor((totalFrames % (frameRate * 3600)) / (frameRate * 60));
var seconds = Math.floor((totalFrames % (frameRate * 60)) / frameRate);
var hours = Math.floor(frameNumber / (fps * 3600)); var frames = totalFrames % Math.round(frameRate);
var minutes = Math.floor((frameNumber % (fps * 3600)) / (fps * 60));
var seconds = Math.floor((frameNumber % (fps * 60)) / fps);
var frames = frameNumber % Math.floor(fps);
result.timeCode = pad(hours, 2) + ":" + pad(minutes, 2) + ":" + result.timeCode = pad(hours, 2) + ":" + pad(minutes, 2) + ":" +
pad(seconds, 2) + ":" + pad(frames, 2); pad(seconds, 2) + ":" + pad(frames, 2);
return JSON.stringify(result); return JSON.stringify(result);
} catch (error) { } catch (error) {
return JSON.stringify(result); return JSON.stringify(result);
} }
} }
// ============================================================================
// Helper Functions
// ============================================================================
/** /**
* Recursively searches for a project item by name * Gets basic project information.
* @param {Bin} bin - The bin to search in * @returns {string} JSON string with project name, path, and sequence count
* @param {string} name - The name to search for
* @returns {ProjectItem} The found project item or 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];
// Check if this item matches
if (item.name === name) {
return item;
}
// If it's a bin, search recursively
if (item.type === "bin") {
var found = findProjectItemByName(item, name);
if (found) {
return found;
}
}
}
return null;
}
/**
* Pads a number with leading zeros
* @param {number} num - The number to pad
* @param {number} digits - The number of digits to pad to
* @returns {string} The padded number
*/
function pad(num, digits) {
var str = "" + num;
while (str.length < digits) {
str = "0" + str;
}
return str;
}
/**
* Gets basic project information
* @returns {string} JSON string with project info
*/ */
function getProjectInfo() { function getProjectInfo() {
var result = { var result = {
projectName: "", projectName: "",
projectPath: "", projectPath: "",
videoSequenceCount: 0, sequenceCount: 0
audioSequenceCount: 0
}; };
try { try {
if (!app.project) { if (!app.project) return JSON.stringify(result);
return JSON.stringify(result);
}
result.projectName = app.project.name; result.projectName = app.project.name;
result.projectPath = app.project.path;
var projectFile = app.project.path;
if (projectFile) {
result.projectPath = projectFile.fsName;
}
// Count sequences
var rootBin = app.project.rootItem; var rootBin = app.project.rootItem;
if (rootBin && rootBin.children) { if (rootBin && rootBin.children) {
for (var i = 0; i < rootBin.children.numItems; i++) { for (var i = 0; i < rootBin.children.numItems; i++) {
var item = rootBin.children[i]; if (rootBin.children[i].type === ProjectItemType.SEQUENCE) {
if (item.type === "sequence") { result.sequenceCount++;
// Check sequence type by checking tracks
if (item.videoTracks && item.videoTracks.numTracks > 0) {
result.videoSequenceCount++;
}
if (item.audioTracks && item.audioTracks.numTracks > 0) {
result.audioSequenceCount++;
}
} }
} }
} }
return JSON.stringify(result); return JSON.stringify(result);
} catch (error) { } catch (error) {
return JSON.stringify(result); return JSON.stringify(result);
@ -355,15 +250,23 @@ function getProjectInfo() {
} }
/** /**
* Exports the current sequence to a specified location * Exports the current sequence using Adobe Media Encoder.
* @param {string} outputPath - The path where the file should be exported *
* @param {string} presetName - The export preset name (e.g., "Apple ProRes 422 HQ") * AME must be installed. This function launches AME (if not already running)
* @returns {string} JSON string with export status * 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, presetName) { function exportSequence(outputPath, presetPath) {
var result = { var result = {
success: false, success: false,
message: "" message: "",
jobId: null
}; };
try { try {
@ -378,35 +281,66 @@ function exportSequence(outputPath, presetName) {
return JSON.stringify(result); return JSON.stringify(result);
} }
var outputFile = new File(outputPath); // Ensure Adobe Media Encoder is running before queuing
app.encoder.launchEncoder();
// Get the export preset // encodeSequence(sequence, outputFilePath, presetPath, workAreaType, removeOnCompletion)
// Note: This requires the preset to be available in Premiere Pro // workAreaType: ENCODE_ENTIRE = 0, ENCODE_IN_TO_OUT = 1
var preset = app.getExportPreset(presetName); var jobId = app.encoder.encodeSequence(
sequence,
outputPath,
presetPath || "",
app.encoder.ENCODE_ENTIRE,
false // keep job in AME queue after completion
);
if (!preset) { if (jobId) {
result.message = "Export preset not found: " + presetName; result.success = true;
return JSON.stringify(result); 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";
} }
// Export the sequence
app.project.exportSequenceToFile(sequence, outputFile, preset);
result.success = true;
result.message = "Export completed";
return JSON.stringify(result); return JSON.stringify(result);
} catch (error) { } catch (error) {
result.message = "Error exporting: " + error.message; result.message = "Export error: " + error.message;
return JSON.stringify(result); return JSON.stringify(result);
} }
} }
// ============================================================================ // ============================================================================
// Initialization // Helper Functions
// ============================================================================ // ============================================================================
// Log that the script has loaded /**
if (typeof(alert) !== "undefined") { * Recursively searches for a project item by display name.
// alert("Wild Dragon MAM ExtendScript loaded"); * @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;
} }