dragonflight/services/premiere-plugin/jsx/premiere.jsx

471 lines
15 KiB
React
Raw Normal View History

/**
* 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;
}