/** * Wild Dragon MAM - Premiere Pro ExtendScript * * This file runs in the Premiere Pro host context (not the browser panel). * It is registered via 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; }