fix(premiere-plugin): CSInterface init, correct API prefix, Node.js download, lazy thumbnails, proper ExtendScript export API: premiere.jsx
This commit is contained in:
parent
a239e30ef2
commit
668e7c6c24
1 changed files with 114 additions and 180 deletions
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue