app
Premiere Pro Bridge
Public Made by Adomby adom
Let the AI edit video in Adobe Premiere Pro on your desktop (import, cut, trim, razor, effects, audio, previews, export) with no mouse or keyboard. Check adom-bridge bridge_list for 'premiere'; install with bridge_install + adom-wiki pkg install adom/premiere-bridge, then start with premiere_status.
main
anastasia
Premiere Pro Bridge 0.1.0: Rust bridge, UXP plugin, CEP helper, skills, macOS manifest
32fb583
10d ago
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700
// Adom Bridge — UXP plugin for Adobe Premiere Pro.
//
// Dials OUT to the Rust bridge's loopback WebSocket (UXP can't host a server),
// receives {type:"call", id, verb, args}, runs the verb against the `premierepro`
// API and answers {type:"result", id, ok, result|error}.
//
// Rules this file lives by:
// * Since Premiere 26.3 every create*Action must be built INSIDE project.lockedAccess,
// and lockedAccess callbacks are synchronous — so do every async read first, then
// build actions in one synchronous pass (see tx()).
// * Anything we hand back must be JSON-safe: times go out as {seconds, ticks, timecode}.
const ppro = require("premierepro");
const fs = require("fs");
const os = require("os");
const PLUGIN_VERSION = "0.1.0";
const PROTOCOL = 1;
const C = ppro.Constants;
const TICKS_PER_SECOND = 254016000000;
// ───────────────────────────── UI ─────────────────────────────
const ui = {
led: document.getElementById("led"),
status: document.getElementById("status"),
last: document.getElementById("last"),
log: document.getElementById("log"),
};
const logLines = [];
function log(msg) {
const line = `${new Date().toLocaleTimeString()} ${msg}`;
logLines.push(line);
if (logLines.length > 40) logLines.shift();
if (ui.log) ui.log.textContent = logLines.join("\n");
console.log("[adom]", msg);
}
function setStatus(state, text) {
if (ui.led) ui.led.className = "led " + (state === "ok" ? "ok" : "wait");
if (ui.status) ui.status.textContent = text;
}
// ───────────────────────────── connection ─────────────────────────────
let ws = null;
let backoff = 1000;
let connecting = false;
function connectionPaths() {
const home = (os.homedir && os.homedir()) || "";
const sep = home.includes("\\") ? "\\" : "/";
const p = [home, ".adom", "premiere-bridge", "connection.json"].join(sep);
return [p, "file:" + p, "plugin:/connection.json"];
}
async function readConnection() {
for (const p of connectionPaths()) {
try {
const txt = await fs.readFile(p, { encoding: "utf-8" });
const j = JSON.parse(txt);
if (j && j.port && j.token) return j;
} catch (_) {
/* try the next location */
}
}
return null;
}
async function connect() {
if (connecting || (ws && ws.readyState <= 1)) return;
connecting = true;
try {
const conn = await readConnection();
if (!conn) {
setStatus("wait", "Waiting for Adom Bridge (no connection file yet)…");
return scheduleReconnect();
}
const url = `ws://127.0.0.1:${conn.port}/ws?token=${encodeURIComponent(conn.token)}`;
setStatus("wait", `Connecting to bridge on port ${conn.port}…`);
const sock = new WebSocket(url);
ws = sock;
sock.onopen = async () => {
backoff = 1000;
setStatus("ok", `Connected (port ${conn.port})`);
log("connected");
send({ type: "hello", runtime: "uxp", ...(await helloInfo()) });
};
sock.onmessage = (ev) => onMessage(ev.data);
sock.onclose = () => {
if (ws === sock) ws = null;
setStatus("wait", "Disconnected — retrying…");
scheduleReconnect();
};
sock.onerror = () => {
/* onclose follows */
};
} catch (e) {
log("connect error: " + e);
scheduleReconnect();
} finally {
connecting = false;
}
}
function scheduleReconnect() {
setTimeout(connect, backoff);
backoff = Math.min(backoff * 2, 5000);
}
function send(obj) {
if (ws && ws.readyState === 1) ws.send(JSON.stringify(obj, jsonReplacer));
}
async function helloInfo() {
let project = null;
let sequence = null;
try {
const p = await ppro.Project.getActiveProject();
if (p) {
project = p.name;
const s = await p.getActiveSequence();
if (s) sequence = s.name;
}
} catch (_) {}
let premiereVersion = "?";
try {
const host = require("uxp").host;
premiereVersion = host.version;
} catch (_) {}
return { pluginVersion: PLUGIN_VERSION, protocol: PROTOCOL, premiereVersion, project, sequence, os: os.platform && os.platform() };
}
async function onMessage(data) {
let msg;
try {
msg = JSON.parse(data);
} catch (_) {
return;
}
if (msg.type === "ping") return send({ type: "pong" });
if (msg.type !== "call") return;
const { id, verb, args } = msg;
const fn = handlers[verb];
if (ui.last) ui.last.textContent = `Last: ${verb}`;
if (!fn) {
return send({ type: "result", id, ok: false, errorCode: "not_in_uxp", error: `UXP plugin has no verb '${verb}'` });
}
const warnings = [];
try {
const result = await fn(args || {}, { warn: (w) => warnings.push(w) });
send({ type: "result", id, ok: true, result: result === undefined ? null : result, warnings: warnings.length ? warnings : undefined });
} catch (e) {
log(`${verb} failed: ${e && e.message ? e.message : e}`);
send({
type: "result",
id,
ok: false,
error: String(e && e.message ? e.message : e),
errorCode: (e && e.code) || "premiere_error",
stack: e && e.stack ? String(e.stack).split("\n").slice(0, 6).join("\n") : undefined,
});
}
}
function fail(code, message) {
const e = new Error(message);
e.code = code;
return e;
}
function jsonReplacer(_k, v) {
if (v && typeof v === "object" && typeof v.ticks === "string" && typeof v.seconds === "number" && !("timecode" in v)) {
return { seconds: v.seconds, ticks: v.ticks };
}
if (typeof v === "bigint") return v.toString();
return v;
}
// ───────────────────────────── helpers: project / sequence ─────────────────────────────
async function project() {
const p = await ppro.Project.getActiveProject();
if (!p) throw fail("no_project", "No project is open in Premiere. Use premiere_open_project or premiere_new_project.");
return p;
}
async function findSequence(p, ref) {
if (ref === undefined || ref === null || ref === "") {
const s = await p.getActiveSequence();
if (!s) throw fail("no_sequence", "No active sequence. Use premiere_create_sequence or premiere_set_active_sequence.");
return s;
}
const seqs = await p.getSequences();
for (const s of seqs) {
if (s.guid.toString() === String(ref) || s.name === String(ref)) return s;
}
throw fail("sequence_not_found", `No sequence matching '${ref}'. Sequences: ${seqs.map((s) => s.name).join(", ")}`);
}
async function seqCtx(args) {
const p = await project();
const seq = await findSequence(p, args.sequence);
return { p, seq };
}
async function frameRate(seq) {
const settings = await seq.getSettings();
return settings.getVideoFrameRate();
}
async function timeDisplay(seq) {
return await seq.getSequenceVideoTimeDisplayFormat();
}
/** Seconds (number), timecode ("00:00:05:12"), "5s", "120f", {seconds}|{ticks}|{frames}. */
async function toTick(v, seq) {
if (v === undefined || v === null) return undefined;
if (typeof v === "object" && v.ticks !== undefined) return ppro.TickTime.createWithTicks(String(v.ticks));
if (typeof v === "object" && v.seconds !== undefined) return ppro.TickTime.createWithSeconds(Number(v.seconds));
if (typeof v === "object" && v.frames !== undefined) {
return ppro.TickTime.createWithFrameAndFrameRate(Number(v.frames), await frameRate(seq));
}
if (typeof v === "number") return ppro.TickTime.createWithSeconds(v);
const s = String(v).trim();
if (/^-?\d+(\.\d+)?s?$/.test(s)) return ppro.TickTime.createWithSeconds(parseFloat(s));
if (/^\d+f$/.test(s)) return ppro.TickTime.createWithFrameAndFrameRate(parseInt(s, 10), await frameRate(seq));
if (/^\d+[:;]\d+[:;]\d+[:;]\d+$/.test(s)) {
const fr = await frameRate(seq);
return ppro.TickTime.createWithSeconds(timecodeToSeconds(s, fr.value));
}
throw fail("bad_time", `Can't parse time '${v}'. Use seconds (12.5), a timecode ("00:00:12:15"), "300f", or {ticks}.`);
}
async function timeOut(t, seq) {
if (!t) return null;
const o = { seconds: round(t.seconds), ticks: t.ticks };
if (seq) {
try {
const fr = await frameRate(seq);
o.timecode = secondsToTimecode(t.seconds, fr.value);
o.frame = Math.round((t.ticksNumber || Number(t.ticks)) / fr.ticksPerFrame);
} catch (_) {}
}
return o;
}
// 29.97/59.94 use drop-frame (";" separator) like Premiere's default display.
function isDropFrame(fps) {
return Math.abs(fps - 29.97) < 0.01 || Math.abs(fps - 59.94) < 0.01;
}
function secondsToTimecode(seconds, fps) {
const nominal = Math.round(fps);
let frames = Math.round(seconds * fps);
const drop = isDropFrame(fps);
if (drop) {
const d = nominal === 60 ? 4 : 2; // frames dropped per minute
const perMin = nominal * 60 - d;
const per10 = perMin * 10 + d;
const tens = Math.floor(frames / per10);
const rem = frames % per10;
frames += d * 9 * tens + (rem > d ? d * Math.floor((rem - d) / perMin) : 0);
}
const ff = frames % nominal;
const totalSec = Math.floor(frames / nominal);
const pad = (n) => String(n).padStart(2, "0");
const sep = drop ? ";" : ":";
return `${pad(Math.floor(totalSec / 3600))}:${pad(Math.floor(totalSec / 60) % 60)}:${pad(totalSec % 60)}${sep}${pad(ff)}`;
}
function timecodeToSeconds(tc, fps) {
const parts = tc.split(/[:;]/).map((n) => parseInt(n, 10));
const [hh, mm, ss, ff] = parts;
const nominal = Math.round(fps);
let frames = ((hh * 60 + mm) * 60 + ss) * nominal + ff;
if (isDropFrame(fps) && /;/.test(tc)) {
const d = nominal === 60 ? 4 : 2;
const totalMin = hh * 60 + mm;
frames -= d * (totalMin - Math.floor(totalMin / 10));
}
return frames / fps;
}
function round(n) {
return Math.round(n * 1000) / 1000;
}
/** Run actions as ONE undoable step. `build` runs synchronously inside lockedAccess. */
function tx(p, label, build) {
let ok = false;
let err = null;
p.lockedAccess(() => {
try {
ok = p.executeTransaction((ca) => {
const actions = build();
for (const a of [].concat(actions).filter(Boolean)) ca.addAction(a);
}, label || "Adom Bridge");
} catch (e) {
err = e;
}
});
if (err) throw err;
if (ok === false) throw fail("transaction_failed", `Premiere rejected the edit '${label}'. Check the clip/time arguments (e.g. a trim past the media's length).`);
return true;
}
// ───────────────────────────── helpers: project items ─────────────────────────────
// Item type numbers come from ppro.ProjectItem.TYPE_* (Constants.ProjectItemType isn't
// exposed at runtime on 26.x).
function typeName(t) {
const PI = ppro.ProjectItem;
const map = { [PI.TYPE_BIN]: "bin", [PI.TYPE_CLIP]: "clip", [PI.TYPE_FILE]: "file", [PI.TYPE_ROOT]: "root" };
if (PI.TYPE_COMPOUND !== undefined) map[PI.TYPE_COMPOUND] = "compound";
if (PI.TYPE_STYLE !== undefined) map[PI.TYPE_STYLE] = "style";
return map[t] || String(t);
}
async function walkItems(folder, path, out, depth, maxDepth) {
const items = await folder.getItems();
for (const it of items) {
const type = typeName(it.type);
const rec = { item: it, name: it.name, type, bin: path, id: safe(() => it.getId()) };
out.push(rec);
if (type === "bin" && depth < maxDepth) {
await walkItems(ppro.FolderItem.cast(it), path ? `${path}/${it.name}` : it.name, out, depth + 1, maxDepth);
}
}
return out;
}
function safe(fn, dflt = null) {
try {
return fn();
} catch (_) {
return dflt;
}
}
async function allItems(p) {
return walkItems(await p.getRootItem(), "", [], 0, 32);
}
/** Item ref: id string, exact name, bin path "Footage/clip.mp4", or {path: mediaFilePath}. */
async function findItem(p, ref) {
if (!ref) throw fail("bad_args", "item is required (project item id, name, 'Bin/name', or {mediaPath})");
const recs = await allItems(p);
const want = typeof ref === "object" ? ref : { key: String(ref) };
for (const r of recs) {
if (want.key !== undefined) {
if (r.id === want.key || r.name === want.key || (r.bin ? `${r.bin}/${r.name}` : r.name) === want.key) return r.item;
}
}
const mediaPath = want.mediaPath || want.path || (want.key && /[\\/]/.test(want.key) ? want.key : null);
if (mediaPath) {
for (const r of recs) {
if (r.type !== "clip" && r.type !== "file") continue;
const clip = ppro.ClipProjectItem.cast(r.item);
const mp = clip ? await clip.getMediaFilePath().catch(() => "") : "";
if (mp && normPath(mp) === normPath(mediaPath)) return r.item;
}
}
throw fail("item_not_found", `No project item matching ${JSON.stringify(ref)}. Call premiere_list_project_items to see ids and names.`);
}
function normPath(s) {
return String(s).replace(/\\/g, "/").toLowerCase();
}
async function describeItem(rec, withMedia) {
const o = { id: rec.id, name: rec.name, type: rec.type, bin: rec.bin || "/" };
if (withMedia && (rec.type === "clip" || rec.type === "file")) {
const clip = ppro.ClipProjectItem.cast(rec.item);
if (clip) {
o.mediaPath = await clip.getMediaFilePath().catch(() => null);
o.isSequence = await clip.isSequence().catch(() => false);
o.offline = await clip.isOffline().catch(() => null);
try {
const media = await clip.getMedia();
if (media) o.duration = round((await media.duration).seconds);
} catch (_) {}
}
}
return o;
}
async function binFor(p, ref) {
if (!ref || ref === "/" ) return await p.getRootItem();
const it = await findItem(p, ref);
const f = ppro.FolderItem.cast(it);
if (!f) throw fail("not_a_bin", `'${ref}' is not a bin`);
return f;
}
// ───────────────────────────── helpers: tracks & clips ─────────────────────────────
/** "V1" / "A2" / {type:"video", index:0}. Track numbers in refs are 1-based like the UI. */
function parseTrack(ref) {
if (ref && typeof ref === "object") {
return { kind: String(ref.type || ref.kind || "video").startsWith("a") ? "audio" : "video", index: Number(ref.index || 0) };
}
const m = /^([VvAa])(\d+)$/.exec(String(ref || "V1").trim());
if (!m) throw fail("bad_track", `Bad track '${ref}'. Use "V1", "V2", "A1"…`);
return { kind: m[1].toUpperCase() === "V" ? "video" : "audio", index: parseInt(m[2], 10) - 1 };
}
function trackLabel(kind, index) {
return (kind === "video" ? "V" : "A") + (index + 1);
}
async function getTrack(seq, kind, index) {
const count = kind === "video" ? await seq.getVideoTrackCount() : await seq.getAudioTrackCount();
if (index < 0 || index >= count) throw fail("track_not_found", `${trackLabel(kind, index)} doesn't exist (sequence has ${count} ${kind} tracks). premiere_add_tracks can add more.`);
return kind === "video" ? await seq.getVideoTrack(index) : await seq.getAudioTrack(index);
}
async function trackClips(track) {
const items = track.getTrackItems(C.TrackItemType.CLIP, false) || [];
const withStart = [];
for (const it of items) withStart.push({ it, start: (await it.getStartTime()).ticksNumber });
withStart.sort((a, b) => a.start - b.start);
return withStart.map((x) => x.it);
}
/** Clip ref: "V1:0" (track:index, 0-based in time order), {track:"V1", index}, or {track:"V1", at: seconds}. */
async function findClip(seq, ref) {
let trackRef, index, at;
if (typeof ref === "string") {
const m = /^([VvAa]\d+):(\d+)$/.exec(ref.trim());
if (!m) throw fail("bad_clip", `Bad clip ref '${ref}'. Use "V1:0" (track:index) — premiere_get_sequence lists them.`);
trackRef = m[1];
index = parseInt(m[2], 10);
} else if (ref && typeof ref === "object") {
trackRef = ref.track;
index = ref.index;
at = ref.at;
} else {
throw fail("bad_clip", "clip is required, e.g. \"V1:0\"");
}
const { kind, index: tIdx } = parseTrack(trackRef);
const track = await getTrack(seq, kind, tIdx);
const clips = await trackClips(track);
if (at !== undefined) {
const t = (await toTick(at, seq)).ticksNumber;
for (let i = 0; i < clips.length; i++) {
const s = (await clips[i].getStartTime()).ticksNumber;
const e = (await clips[i].getEndTime()).ticksNumber;
if (t >= s && t < e) return { clip: clips[i], kind, trackIndex: tIdx, index: i, ref: `${trackLabel(kind, tIdx)}:${i}` };
}
throw fail("clip_not_found", `No clip on ${trackLabel(kind, tIdx)} at ${at}`);
}
if (index === undefined || index < 0 || index >= clips.length) {
throw fail("clip_not_found", `${trackLabel(kind, tIdx)} has ${clips.length} clips; index ${index} is out of range. premiere_get_sequence lists them.`);
}
return { clip: clips[index], kind, trackIndex: tIdx, index, ref: `${trackLabel(kind, tIdx)}:${index}` };
}
async function describeClip(c, seq, kind, tIdx, i, detail) {
const o = {
ref: `${trackLabel(kind, tIdx)}:${i}`,
name: await c.getName().catch(() => null),
start: await timeOut(await c.getStartTime(), seq),
end: await timeOut(await c.getEndTime(), seq),
duration: round((await c.getDuration()).seconds),
inPoint: round((await c.getInPoint()).seconds),
outPoint: round((await c.getOutPoint()).seconds),
disabled: await c.isDisabled().catch(() => false),
selected: await c.getIsSelected().catch(() => false),
};
if (detail) {
o.speed = await c.getSpeed().catch(() => null);
o.isAdjustmentLayer = await c.isAdjustmentLayer().catch(() => false);
try {
const pi = await c.getProjectItem();
if (pi) {
o.projectItem = { id: safe(() => pi.getId()), name: pi.name };
const clip = ppro.ClipProjectItem.cast(pi);
if (clip) o.projectItem.mediaPath = await clip.getMediaFilePath().catch(() => null);
}
} catch (_) {}
try {
const chain = await c.getComponentChain();
const names = [];
for (let k = 0; k < chain.getComponentCount(); k++) names.push(await chain.getComponentAtIndex(k).getDisplayName());
o.effects = names;
} catch (_) {}
}
return o;
}
/** Clips on the other tracks that are linked to this one (same source, same start/end). */
async function linkedPartners(seq, target) {
const pi = await target.clip.getProjectItem().catch(() => null);
const id = pi && safe(() => pi.getId());
const S = (await target.clip.getStartTime()).ticks;
const E = (await target.clip.getEndTime()).ticks;
const out = [];
for (const kind of ["video", "audio"]) {
const n = kind === "video" ? await seq.getVideoTrackCount() : await seq.getAudioTrackCount();
for (let ti = 0; ti < n; ti++) {
if (kind === target.kind && ti === target.trackIndex) continue;
const clips = await trackClips(await getTrack(seq, kind, ti));
for (let i = 0; i < clips.length; i++) {
const c = clips[i];
if ((await c.getStartTime()).ticks !== S || (await c.getEndTime()).ticks !== E) continue;
const cpi = await c.getProjectItem().catch(() => null);
if (id && cpi && safe(() => cpi.getId()) === id) out.push({ clip: c, kind, trackIndex: ti, index: i, ref: `${trackLabel(kind, ti)}:${i}` });
}
}
}
return out;
}
/** Targets plus their linked partners (unless args.linked === false), de-duplicated. */
async function withLinked(seq, args, targets) {
if (args.linked === false) return targets;
const all = [...targets];
for (const t of targets) {
for (const p of await linkedPartners(seq, t)) if (!all.some((x) => x.ref === p.ref)) all.push(p);
}
return all;
}
/**
* Run `fn(selection)` with a TrackItemSelection holding `clips`. The selection object is
* only valid INSIDE createEmptySelection's callback ("The script object is no longer
* valid" otherwise), so everything that uses it happens in there.
*/
function withSelection(clips, fn) {
let result;
let err = null;
const ok = ppro.TrackItemSelection.createEmptySelection((sel) => {
try {
for (const c of clips) sel.addItem(c, true);
result = fn(sel);
} catch (e) {
err = e;
}
});
if (err) throw err;
if (ok === false) throw fail("selection_failed", "Couldn't create a track item selection");
return result;
}
async function resolveClips(seq, args) {
const refs = args.clips || (args.clip !== undefined ? [args.clip] : null);
if (!refs) throw fail("bad_args", "clip or clips is required");
const out = [];
for (const r of refs) out.push(await findClip(seq, r));
return out;
}
// ───────────────────────────── helpers: components & params ─────────────────────────────
async function componentsOf(clip) {
const chain = await clip.getComponentChain();
const comps = [];
for (let i = 0; i < chain.getComponentCount(); i++) {
const c = chain.getComponentAtIndex(i);
comps.push({ index: i, comp: c, displayName: await c.getDisplayName(), matchName: await c.getMatchName() });
}
return { chain, comps };
}
async function findComponent(clip, ref) {
const { chain, comps } = await componentsOf(clip);
const r = ref === undefined ? 0 : ref;
const found =
typeof r === "number"
? comps[r]
: comps.find((c) => c.matchName === r || c.displayName.toLowerCase() === String(r).toLowerCase());
if (!found) {
throw fail("component_not_found", `No component '${ref}' on this clip. It has: ${comps.map((c) => `${c.displayName} (${c.matchName})`).join(", ")}`);
}
return { chain, ...found };
}
function findParam(comp, ref) {
const n = comp.getParamCount();
const names = [];
for (let i = 0; i < n; i++) {
const p = comp.getParam(i);
names.push(p.displayName);
if ((typeof ref === "number" && ref === i) || (typeof ref === "string" && p.displayName.toLowerCase() === ref.toLowerCase())) {
return { param: p, index: i };
}
}
throw fail("param_not_found", `No parameter '${ref}'. Parameters: ${names.join(", ")}`);
}
/**
* Keyframe positions live in the clip's MEDIA time (timeline time - clip start + clip in
* point), measured on 26.5. Verbs take sequence time by default (timeBase:"clip" opts out).
*/
async function kfTime(clip, v, seq, timeBase) {
const t = await toTick(v, seq);
if (timeBase === "clip") return t;
return t.subtract(await clip.getStartTime()).add(await clip.getInPoint());
}
async function kfToSeq(clip, t) {
return t.subtract(await clip.getInPoint()).add(await clip.getStartTime());
}
function makeValue(v) {
if (Array.isArray(v) && v.length === 2) return new ppro.PointF(Number(v[0]), Number(v[1]));
if (v && typeof v === "object" && "x" in v && "y" in v) return new ppro.PointF(Number(v.x), Number(v.y));
if (v && typeof v === "object" && "red" in v) return new ppro.Color(v.red, v.green, v.blue, v.alpha === undefined ? 1 : v.alpha);
return v;
}
async function paramValueAt(param, t) {
try {
const r = await param.getValueAtTime(t);
return r && r.value !== undefined ? r.value : r;
} catch (_) {
try {
const k = await param.getStartValue();
return k && k.value !== undefined ? (k.value.value !== undefined ? k.value.value : k.value) : k;
} catch (_) {
return null;
}
}
}
// Premiere's clip "Volume > Level" param stores linear amplitude; the UI shows dB with
// 0 dB at the param's 1.0-equivalent. These match the long-standing ExtendScript mapping
// (level = 10^((dB-15)/20)); premiere_audio_probe verifies it on the running build.
function dbToLevel(db) {
return Math.pow(10, (Number(db) - 15) / 20);
}
function levelToDb(level) {
return level > 0 ? 20 * Math.log10(level) + 15 : -Infinity;
}
async function volumeParam(clip) {
const { comps } = await componentsOf(clip);
const vol = comps.find((c) => /volume/i.test(c.displayName) || /ADBE Volume/i.test(c.matchName));
if (!vol) throw fail("no_volume", "This clip has no Volume component — is it an audio clip? Use an A-track ref like \"A1:0\".");
const { param } = findParam(vol.comp, "Level");
return param;
}
// ───────────────────────────── handlers ─────────────────────────────
const handlers = {
// ── app / project ──
async ping() {
return { pong: true, ...(await helloInfo()) };
},
async get_project() {
const p = await project();
const seqs = await p.getSequences();
const active = await p.getActiveSequence();
const recs = await allItems(p);
return {
name: p.name,
path: p.path,
guid: p.guid.toString(),
activeSequence: active ? active.name : null,
sequences: seqs.map((s) => ({ name: s.name, guid: s.guid.toString() })),
itemCount: recs.length,
bins: recs.filter((r) => r.type === "bin").map((r) => (r.bin ? `${r.bin}/${r.name}` : r.name)),
};
},
async open_project(a) {
if (!a.path) throw fail("bad_args", "path is required");
const opts = new ppro.OpenProjectOptions();
safe(() => opts.setShowConvertProjectDialog(false));
safe(() => opts.setShowLocateFileDialog(false));
safe(() => opts.setShowWarningDialog(false));
safe(() => opts.setAddToMRUList(true));
const p = await ppro.Project.open(a.path, opts);
if (!p) throw fail("open_failed", `Premiere couldn't open ${a.path}`);
return { name: p.name, path: p.path };
},
async new_project(a) {
if (!a.path) throw fail("bad_args", "path is required (…/MyProject.prproj)");
const path = /\.prproj$/i.test(a.path) ? a.path : a.path + ".prproj";
const p = await ppro.Project.createProject(path);
if (!p) throw fail("create_failed", `Couldn't create ${path} — does the folder exist?`);
return { name: p.name, path: p.path };
},
async save_project(a) {
const p = await project();
if (a.path) return { saved: await p.saveAs(a.path), path: a.path };
return { saved: await p.save(), path: p.path };
},
async close_project(a) {
const p = await project();
if (a.save) await p.save();
const opts = new ppro.CloseProjectOptions();
safe(() => opts.setPromptIfDirty(false));
safe(() => opts.setShowCancelButton(false));
return { closed: await p.close(opts) };
},
// ── project panel ──
async list_project_items(a) {
const p = await project();
let recs = await allItems(p);
if (a.bin) recs = recs.filter((r) => r.bin === a.bin || r.bin.startsWith(a.bin + "/"));
if (a.type) recs = recs.filter((r) => r.type === a.type);
if (a.search) {
const q = String(a.search).toLowerCase();
recs = recs.filter((r) => r.name.toLowerCase().includes(q));
}
const limit = a.limit || 500;
const items = [];
for (const r of recs.slice(0, limit)) items.push(await describeItem(r, a.details !== false));
return { count: recs.length, items, truncated: recs.length > limit };
},
async import_media(a) {
const paths = a.paths || (a.path ? [a.path] : null);
if (!paths || !paths.length) throw fail("bad_args", "paths (array of absolute desktop file paths) is required");
const p = await project();
const bin = await binFor(p, a.bin);
const before = new Set((await allItems(p)).map((r) => r.id));
const ok = await p.importFiles(paths, true, bin, !!a.asNumberedStills);
const after = await allItems(p);
const added = [];
for (const r of after) if (!before.has(r.id)) added.push(await describeItem(r, true));
if (!ok && !added.length) throw fail("import_failed", `Premiere refused the import. Check the paths exist on THIS machine: ${paths.join(", ")}`);
return { imported: added.length, items: added };
},
async create_bin(a) {
const p = await project();
const parent = await binFor(p, a.parent);
tx(p, "Create Bin", () => parent.createBinAction(a.name, a.makeUnique !== false));
return { created: a.name, parent: a.parent || "/" };
},
async move_item(a) {
const p = await project();
const item = await findItem(p, a.item);
const dest = await binFor(p, a.bin);
const parent = item.getParentBin();
tx(p, "Move Item", () => parent.createMoveItemAction(item, dest));
return { moved: item.name, to: a.bin || "/" };
},
async rename_item(a) {
const p = await project();
const item = await findItem(p, a.item);
tx(p, "Rename Item", () => item.createSetNameAction(a.name));
return { renamed: a.name };
},
async delete_item(a) {
const p = await project();
const item = await findItem(p, a.item);
const parent = item.getParentBin();
tx(p, "Delete Item", () => parent.createRemoveItemAction(item));
return { deleted: item.name };
},
async set_item_color(a) {
const p = await project();
const item = await findItem(p, a.item);
tx(p, "Label Color", () => item.createSetColorLabelAction(Number(a.colorIndex)));
return { item: item.name, colorIndex: a.colorIndex };
},
async set_source_in_out(a) {
const p = await project();
const clip = ppro.ClipProjectItem.cast(await findItem(p, a.item));
if (!clip) throw fail("not_a_clip", "item is not a clip");
if (a.clear) {
tx(p, "Clear In/Out", () => clip.createClearInOutPointsAction());
return { cleared: true };
}
const tin = await toTick(a.in, null);
const tout = await toTick(a.out, null);
tx(p, "Source In/Out", () => {
if (tin && tout) return clip.createSetInOutPointsAction(tin, tout);
if (tin) return clip.createSetInPointAction(tin);
if (tout) return clip.createSetOutPointAction(tout);
return [];
});
return { item: clip.name, in: a.in, out: a.out };
},
async create_subclip(a) {
const p = await project();
const clip = ppro.ClipProjectItem.cast(await findItem(p, a.item));
const s = await toTick(a.start, null);
const e = await toTick(a.end, null);
tx(p, "Subclip", () => clip.createSubClipAction(a.name || `${clip.name} subclip`, s, e, !!a.hardBoundaries, { takeVideo: a.video !== false, takeAudio: a.audio !== false }));
return { created: a.name || `${clip.name} subclip` };
},
async get_metadata(a) {
const p = await project();
const item = await findItem(p, a.item);
return {
projectMetadata: await ppro.Metadata.getProjectMetadata(item).catch(() => null),
xmp: a.xmp ? await ppro.Metadata.getXMPMetadata(item).catch(() => null) : undefined,
};
},
async set_xmp_metadata(a) {
const p = await project();
const item = await findItem(p, a.item);
tx(p, "Set Metadata", () => ppro.Metadata.createSetXMPMetadataAction(item, a.xmp));
return { item: item.name };
},
// ── sequences ──
async list_sequences() {
const p = await project();
const active = await p.getActiveSequence();
const out = [];
for (const s of await p.getSequences()) {
out.push({
name: s.name,
guid: s.guid.toString(),
active: !!active && active.guid.toString() === s.guid.toString(),
duration: round((await s.getEndTime()).seconds),
});
}
return { sequences: out };
},
async set_active_sequence(a) {
const p = await project();
const s = await findSequence(p, a.sequence);
await p.openSequence(s).catch(() => {});
await p.setActiveSequence(s);
return { active: s.name };
},
async create_sequence(a) {
const p = await project();
let s;
if (a.fromItems && a.fromItems.length) {
// Takes plain ProjectItems; passing an undefined bin is an "Illegal Parameter type".
const items = [];
for (const r of a.fromItems) items.push(await findItem(p, r));
s = a.bin ? await p.createSequenceFromMedia(a.name || "Sequence", items, await binFor(p, a.bin)) : await p.createSequenceFromMedia(a.name || "Sequence", items);
} else if (a.presetPath) {
s = await p.createSequenceWithPresetPath(a.name || "Sequence", a.presetPath);
} else {
s = await p.createSequence(a.name || "Sequence");
}
if (!s) throw fail("create_failed", "Premiere didn't create the sequence");
await p.setActiveSequence(s).catch(() => {});
return { name: s.name, guid: s.guid.toString() };
},
async duplicate_sequence(a) {
const { p, seq } = await seqCtx(a);
tx(p, "Duplicate Sequence", () => seq.createCloneAction());
return { duplicated: seq.name };
},
async delete_sequence(a) {
const { p, seq } = await seqCtx(a);
return { deleted: await p.deleteSequence(seq), name: seq.name };
},
async nest_selection(a) {
const { seq } = await seqCtx(a);
const nested = await seq.createSubsequence(!!a.ignoreTrackTargeting);
return { nested: nested ? nested.name : null };
},
async get_sequence(a) {
const { seq } = await seqCtx(a);
const settings = await seq.getSettings();
const fr = settings.getVideoFrameRate();
const rect = await seq.getFrameSize();
const out = {
name: seq.name,
guid: seq.guid.toString(),
width: rect.width,
height: rect.height,
fps: round(fr.value || TICKS_PER_SECOND / fr.ticksPerFrame),
duration: await timeOut(await seq.getEndTime(), seq),
playhead: await timeOut(await seq.getPlayerPosition(), seq),
inPoint: await timeOut(await seq.getInPoint(), seq),
outPoint: await timeOut(await seq.getOutPoint(), seq),
videoTracks: [],
audioTracks: [],
};
const detail = a.detail !== false;
for (const kind of ["video", "audio"]) {
const n = kind === "video" ? await seq.getVideoTrackCount() : await seq.getAudioTrackCount();
for (let t = 0; t < n; t++) {
const track = await getTrack(seq, kind, t);
const clips = await trackClips(track);
const desc = [];
for (let i = 0; i < clips.length; i++) desc.push(await describeClip(clips[i], seq, kind, t, i, detail));
(kind === "video" ? out.videoTracks : out.audioTracks).push({
track: trackLabel(kind, t),
name: track.name,
muted: await track.isMuted().catch(() => false),
clips: desc,
});
}
}
return out;
},
async get_sequence_settings(a) {
const { seq } = await seqCtx(a);
const s = await seq.getSettings();
const fr = s.getVideoFrameRate();
const rect = await s.getVideoFrameRect();
return {
width: rect.width,
height: rect.height,
fps: round(fr.value),
pixelAspectRatio: await s.getVideoPixelAspectRatio().catch(() => null),
fieldType: await s.getVideoFieldType().catch(() => null),
audioSampleRate: await s.getAudioSampleRate().then((r) => r.value).catch(() => null),
audioChannelCount: await s.getAudioChannelCount().catch(() => null),
editingMode: await s.getEditingMode().catch(() => null),
previewCodec: await s.getPreviewCodec().catch(() => null),
maxBitDepth: await s.getMaximumBitDepth().catch(() => null),
maxRenderQuality: await s.getMaxRenderQuality().catch(() => null),
};
},
async set_sequence_settings(a) {
const { p, seq } = await seqCtx(a);
const s = await seq.getSettings();
if (a.width && a.height) {
const r = new ppro.RectF();
r.width = a.width;
r.height = a.height;
await s.setVideoFrameRect(r);
}
if (a.fps) s.setVideoFrameRate(ppro.FrameRate.createWithValue(Number(a.fps)));
if (a.maxBitDepth !== undefined) await s.setMaximumBitDepth(!!a.maxBitDepth);
if (a.maxRenderQuality !== undefined) await s.setMaxRenderQuality(!!a.maxRenderQuality);
tx(p, "Sequence Settings", () => seq.createSetSettingsAction(s));
return await handlers.get_sequence_settings(a);
},
async set_playhead(a) {
const { seq } = await seqCtx(a);
const t = await toTick(a.time, seq);
await seq.setPlayerPosition(t);
return { playhead: await timeOut(await seq.getPlayerPosition(), seq) };
},
async set_in_out(a) {
const { p, seq } = await seqCtx(a);
const tin = await toTick(a.in, seq);
const tout = await toTick(a.out, seq);
tx(p, "Sequence In/Out", () => [tin && seq.createSetInPointAction(tin), tout && seq.createSetOutPointAction(tout)]);
if (a.workArea) ppro.WorkAreaUtils.setWorkAreaInOutPoints(seq, tin, tout);
return { inPoint: await timeOut(await seq.getInPoint(), seq), outPoint: await timeOut(await seq.getOutPoint(), seq) };
},
// ── timeline editing ──
async insert_clip(a) {
return placeClip(a, "insert");
},
async overwrite_clip(a) {
return placeClip(a, "overwrite");
},
async append_clip(a) {
const { seq } = await seqCtx(a);
const { kind, index } = parseTrack(a.track || "V1");
let end = 0;
const clips = await trackClips(await getTrack(seq, kind, index));
if (clips.length) end = (await clips[clips.length - 1].getEndTime()).seconds;
return placeClip({ ...a, time: end }, "overwrite");
},
async move_clip(a) {
const { p, seq } = await seqCtx(a);
const targets = await withLinked(seq, a, await resolveClips(seq, a));
const plans = [];
for (const t of targets) {
const start = await t.clip.getStartTime();
let delta;
if (a.by !== undefined) delta = await toTick(a.by, seq);
else if (a.to !== undefined) delta = (await toTick(a.to, seq)).subtract(start);
else throw fail("bad_args", "Pass to (new start time) or by (offset, may be negative)");
plans.push({ clip: t.clip, delta });
}
tx(p, "Move Clip", () => plans.map((x) => x.clip.createMoveAction(x.delta)));
return { moved: targets.map((t) => t.ref), _note: "Moving can overwrite neighbors; call premiere_get_sequence to see the result." };
},
async trim_clip(a) {
const { p, seq } = await seqCtx(a);
const target = await findClip(seq, a.clip);
const { clip, ref, kind, trackIndex, index } = target;
const s0 = await toTick(a.start, seq);
const e0 = await toTick(a.end, seq);
const i = await toTick(a.inPoint, seq);
const o = await toTick(a.outPoint, seq);
const ds = a.trimStart !== undefined ? (await clip.getStartTime()).add(await toTick(a.trimStart, seq)) : null;
const de = a.trimEnd !== undefined ? (await clip.getEndTime()).subtract(await toTick(a.trimEnd, seq)) : null;
const group = await withLinked(seq, a, [target]);
tx(p, "Trim Clip", () =>
group.flatMap((g) => [
(s0 || ds) && g.clip.createSetStartAction(s0 || ds),
(e0 || de) && g.clip.createSetEndAction(e0 || de),
i && g.clip.createSetInPointAction(i),
o && g.clip.createSetOutPointAction(o),
])
);
return { clip: ref, linked: group.slice(1).map((g) => g.ref), after: await describeClip(clip, seq, kind, trackIndex, index, false) };
},
async remove_clips(a) {
const { p, seq } = await seqCtx(a);
const targets = await withLinked(seq, a, await resolveClips(seq, a));
const ed = ppro.SequenceEditor.getEditor(seq);
withSelection(targets.map((t) => t.clip), (sel) =>
tx(p, a.ripple ? "Ripple Delete" : "Delete Clips", () => ed.createRemoveItemsAction(sel, !!a.ripple, C.MediaType.ANY, !!a.shiftOverlapping))
);
return { removed: targets.map((t) => t.ref), ripple: !!a.ripple };
},
async remove_gap(a) {
// Ripple-close the gap before a clip: it and everything after it on its track (and on
// its linked partners' tracks, unless linked:false) shift left by the gap.
const { p, seq } = await seqCtx(a);
const target = await findClip(seq, a.clip);
const { clip, kind, trackIndex, index } = target;
const clips = await trackClips(await getTrack(seq, kind, trackIndex));
const prevEnd = index > 0 ? await clips[index - 1].getEndTime() : ppro.TickTime.TIME_ZERO;
const start = await clip.getStartTime();
const gap = start.subtract(prevEnd);
if (gap.ticksNumber <= 0) return { gap: 0 };
const neg = ppro.TickTime.TIME_ZERO.subtract(gap);
const moving = clips.slice(index);
for (const partner of a.linked === false ? [] : await linkedPartners(seq, target)) {
const pcs = await trackClips(await getTrack(seq, partner.kind, partner.trackIndex));
const pPrevEnd = partner.index > 0 ? await pcs[partner.index - 1].getEndTime() : ppro.TickTime.TIME_ZERO;
if (start.subtract(pPrevEnd).ticksNumber < gap.ticksNumber) {
throw fail("gap_blocked", `${partner.ref} can't shift left by ${round(gap.seconds)}s without overlapping the clip before it. Pass linked:false to close the gap on ${trackLabel(kind, trackIndex)} only.`);
}
moving.push(...pcs.slice(partner.index));
}
tx(p, "Close Gap", () => moving.map((c) => c.createMoveAction(neg)));
return { closed: round(gap.seconds), movedClips: moving.length };
},
async set_clip_enabled(a) {
const { p, seq } = await seqCtx(a);
const targets = await resolveClips(seq, a);
tx(p, "Enable/Disable", () => targets.map((t) => t.clip.createSetDisabledAction(a.enabled === false)));
return { clips: targets.map((t) => t.ref), enabled: a.enabled !== false };
},
async rename_clip(a) {
const { p, seq } = await seqCtx(a);
const { clip, ref } = await findClip(seq, a.clip);
tx(p, "Rename Clip", () => clip.createSetNameAction(a.name));
return { clip: ref, name: a.name };
},
async duplicate_clip(a) {
const { p, seq } = await seqCtx(a);
const { clip, ref } = await findClip(seq, a.clip);
const off = (await toTick(a.offset === undefined ? 0 : a.offset, seq)) || ppro.TickTime.TIME_ZERO;
const ed = ppro.SequenceEditor.getEditor(seq);
tx(p, "Duplicate Clip", () => ed.createCloneTrackItemAction(clip, off, Number(a.videoTrackOffset || 0), Number(a.audioTrackOffset || 0), a.alignToVideo !== false, !!a.insert));
return { duplicated: ref };
},
async razor(a, ctx) {
// UXP has no razor. Measured on 26.5: head trims (setStart/setInPoint) keep a clip's
// sync offset (start - in), tail trims only move the end, and any cloned/moved copy
// breaks sync — so a trim+clone razor can't be correct. Instead, for each linked
// V/A group spanning T: trim the tails to T, temporarily set the SOURCE clip's in/out
// to the right-hand half, overwrite it back in at T (same sync offset), then restore
// the source in/out. Effects on the original stay on the left half only; the CEP
// helper's true QE razor (preferred when connected) keeps them on both.
const { p, seq } = await seqCtx(a);
const fr = await frameRate(seq);
let T = await toTick(a.time, seq);
if (T.alignToNearestFrame) T = T.alignToNearestFrame(fr);
const want = a.tracks ? new Set(a.tracks.map((t) => String(t).toUpperCase())) : null;
const spans = [];
for (const kind of ["video", "audio"]) {
const n = kind === "video" ? await seq.getVideoTrackCount() : await seq.getAudioTrackCount();
for (let ti = 0; ti < n; ti++) {
if (want && !want.has(trackLabel(kind, ti))) continue;
for (const c of await trackClips(await getTrack(seq, kind, ti))) {
const S = await c.getStartTime();
const E = await c.getEndTime();
if (T.ticksNumber > S.ticksNumber && T.ticksNumber < E.ticksNumber) {
spans.push({ c, kind, ti, S, E, I: await c.getInPoint(), speed: await c.getSpeed().catch(() => 1), pi: await c.getProjectItem() });
}
}
}
}
if (!spans.length) {
ctx.warn("No clip spans that time on the chosen tracks — nothing was cut.");
return { cutTracks: [], at: await timeOut(T, seq), method: "uxp-overwrite" };
}
// Group linked partners: same source item, same start/end.
const groups = [];
for (const sp of spans) {
const key = `${sp.pi && safe(() => sp.pi.getId())}|${sp.S.ticks}|${sp.E.ticks}`;
let g = groups.find((x) => x.key === key);
if (!g) groups.push((g = { key, members: [] }));
g.members.push(sp);
}
const ed = ppro.SequenceEditor.getEditor(seq);
const cut = [];
for (const g of groups) {
const ref = g.members[0];
if (Math.abs(ref.speed - 1) > 1e-6) throw fail("razor_speed", "This clip has a speed change; the UXP razor fallback can't keep sync. Connect the CEP helper (premiere_install_plugin + restart Premiere) for a true razor.");
const clipItem = ppro.ClipProjectItem.cast(ref.pi);
if (!clipItem) throw fail("razor_failed", "Can't razor a clip without a source project item (e.g. a graphic) in UXP mode.");
const v = g.members.find((m) => m.kind === "video");
const au = g.members.find((m) => m.kind === "audio");
const vt = v ? v.ti : -1;
const at = au ? au.ti : -1;
const left = T.subtract(ref.S);
const newIn = ref.I.add(left);
const newOut = ref.I.add(ref.E.subtract(ref.S));
const mtV = C.MediaType.VIDEO;
const prevIn = await clipItem.getInPoint(v ? mtV : C.MediaType.AUDIO).catch(() => null);
const prevOut = await clipItem.getOutPoint(v ? mtV : C.MediaType.AUDIO).catch(() => null);
tx(p, "Razor (trim)", () => g.members.map((m) => m.c.createSetEndAction(T)));
try {
tx(p, "Razor (source range)", () => clipItem.createSetInOutPointsAction(newIn, newOut));
// Missing V or A in the group: aim that half at a far track index Premiere ignores is
// not available, so overwrite both and then remove the stray half below.
tx(p, "Razor (overwrite)", () => ed.createOverwriteItemAction(ref.pi, T, vt >= 0 ? vt : 0, at >= 0 ? at : 0));
} finally {
tx(p, "Razor (restore source)", () => (prevIn && prevOut ? clipItem.createSetInOutPointsAction(prevIn, prevOut) : clipItem.createClearInOutPointsAction()));
}
// If only one side was razored, the overwrite also laid the other side down at T on
// track 0 — remove that stray piece so other tracks are untouched.
if (vt < 0 || at < 0) {
const strayKind = vt < 0 ? "video" : "audio";
const strayTrack = await getTrack(seq, strayKind, 0);
const stray = await clipStartingAt(strayTrack, T);
if (stray) {
withSelection([stray], (sel) => tx(p, "Razor (cleanup)", () => ed.createRemoveItemsAction(sel, false, C.MediaType.ANY, false)));
ctx.warn(`Overwrite also touched ${trackLabel(strayKind, 0)} at the cut; that piece was removed. Razor linked V+A together to avoid this.`);
}
}
for (const m of g.members) cut.push(trackLabel(m.kind, m.ti));
}
return { cutTracks: cut, at: await timeOut(T, seq), method: "uxp-overwrite" };
},
async select_clips(a) {
const { seq } = await seqCtx(a);
const targets = await withLinked(seq, a, await resolveClips(seq, a));
withSelection(targets.map((t) => t.clip), (sel) => seq.setSelection(sel));
return { selected: targets.map((t) => t.ref) };
},
async get_selection(a) {
const { seq } = await seqCtx(a);
const sel = await seq.getSelection();
const items = await sel.getTrackItems();
const out = [];
for (const it of items) out.push({ name: await it.getName(), start: await timeOut(await it.getStartTime(), seq), end: await timeOut(await it.getEndTime(), seq), track: await it.getTrackIndex() });
return { count: out.length, items: out };
},
async clear_selection(a) {
const { seq } = await seqCtx(a);
return { cleared: await seq.clearSelection() };
},
async scene_edit_detection(a) {
const { seq } = await seqCtx(a);
const targets = await resolveClips(seq, a);
const op = { cut: ppro.SequenceUtils.SEQUENCE_OPERATION_APPLYCUT, marker: ppro.SequenceUtils.SEQUENCE_OPERATION_CREATEMARKER, subclip: ppro.SequenceUtils.SEQUENCE_OPERATION_CREATESUBCLIP }[a.operation || "cut"];
const started = await withSelection(targets.map((t) => t.clip), (sel) => ppro.SequenceUtils.performSceneEditDetectionOnSelection(op, sel));
return { started, operation: a.operation || "cut" };
},
async set_track_mute(a) {
const { seq } = await seqCtx(a);
const { kind, index } = parseTrack(a.track);
const track = await getTrack(seq, kind, index);
await track.setMute(a.mute !== false);
return { track: trackLabel(kind, index), muted: await track.isMuted() };
},
async rename_track(a) {
const { p, seq } = await seqCtx(a);
const { kind, index } = parseTrack(a.track);
const track = await getTrack(seq, kind, index);
tx(p, "Rename Track", () => track.createSetNameAction(a.name));
return { track: trackLabel(kind, index), name: a.name };
},
// ── effects, params, keyframes ──
async list_effects(a) {
const kind = a.kind || "video";
let names;
if (kind === "video") {
const display = await ppro.VideoFilterFactory.getDisplayNames();
const match = await ppro.VideoFilterFactory.getMatchNames();
names = match.map((m, i) => ({ matchName: m, displayName: display[i] }));
} else if (kind === "audio") {
names = (await ppro.AudioFilterFactory.getDisplayNames()).map((d) => ({ displayName: d }));
} else if (kind === "transition") {
names = (await ppro.TransitionFactory.getVideoTransitionMatchNames()).map((m) => ({ matchName: m }));
} else throw fail("bad_args", "kind must be video, audio, or transition");
if (a.search) {
const q = String(a.search).toLowerCase();
names = names.filter((n) => (n.displayName || "").toLowerCase().includes(q) || (n.matchName || "").toLowerCase().includes(q));
}
return { kind, count: names.length, effects: names };
},
async get_clip_effects(a) {
const { seq } = await seqCtx(a);
const { clip, ref } = await findClip(seq, a.clip);
const { comps } = await componentsOf(clip);
const at = a.time !== undefined ? await kfTime(clip, a.time, seq, a.timeBase) : await clip.getInPoint();
const out = [];
for (const c of comps) {
const params = [];
for (let i = 0; i < c.comp.getParamCount(); i++) {
const prm = c.comp.getParam(i);
params.push({
index: i,
name: prm.displayName,
value: await paramValueAt(prm, at),
animated: safe(() => prm.isTimeVarying(), false),
keyframes: await Promise.all(safe(() => prm.getKeyframeListAsTickTimes(), []).map(async (t) => round((await kfToSeq(clip, t)).seconds))),
});
}
out.push({ index: c.index, displayName: c.displayName, matchName: c.matchName, params });
}
return { clip: ref, components: out };
},
async add_effect(a) {
const { p, seq } = await seqCtx(a);
const targets = await resolveClips(seq, a);
const done = [];
for (const t of targets) {
let comp;
if (t.kind === "video") {
let match = a.matchName;
if (!match && a.name) {
const display = await ppro.VideoFilterFactory.getDisplayNames();
const matches = await ppro.VideoFilterFactory.getMatchNames();
const i = display.findIndex((d) => d.toLowerCase() === String(a.name).toLowerCase());
if (i < 0) throw fail("effect_not_found", `No video effect named '${a.name}'. premiere_list_effects {kind:"video", search:"…"} lists them.`);
match = matches[i];
}
comp = await ppro.VideoFilterFactory.createComponent(match);
} else {
comp = await ppro.AudioFilterFactory.createComponentByDisplayName(a.name || a.matchName, t.clip);
}
if (!comp) throw fail("effect_not_found", `Premiere couldn't create effect '${a.name || a.matchName}'`);
const chain = await t.clip.getComponentChain();
tx(p, "Add Effect", () => (a.index !== undefined ? chain.createInsertComponentAction(comp, Number(a.index)) : chain.createAppendComponentAction(comp)));
done.push(t.ref);
}
return { added: a.name || a.matchName, clips: done };
},
async remove_effect(a) {
const { p, seq } = await seqCtx(a);
const { clip, ref } = await findClip(seq, a.clip);
const { chain, comp, displayName } = await findComponent(clip, a.component);
tx(p, "Remove Effect", () => chain.createRemoveComponentAction(comp));
return { clip: ref, removed: displayName };
},
async set_param(a) {
const { p, seq } = await seqCtx(a);
const targets = await resolveClips(seq, a);
const results = [];
for (const t of targets) {
const { comp, displayName } = await findComponent(t.clip, a.component);
const { param } = findParam(comp, a.param);
const kf = param.createKeyframe(makeValue(a.value));
if (a.time !== undefined) {
kf.position = await kfTime(t.clip, a.time, seq, a.timeBase);
const animated = safe(() => param.isTimeVarying(), false);
tx(p, "Keyframe", () => [!animated && param.createSetTimeVaryingAction(true), param.createAddKeyframeAction(kf)]);
} else {
tx(p, "Set Param", () => param.createSetValueAction(kf, true));
}
results.push({ clip: t.ref, component: displayName, param: param.displayName, value: a.value, keyframedAt: a.time });
}
return { set: results };
},
async add_keyframes(a) {
const { p, seq } = await seqCtx(a);
const { clip, ref } = await findClip(seq, a.clip);
const { comp } = await findComponent(clip, a.component);
const { param } = findParam(comp, a.param);
if (!Array.isArray(a.keyframes) || !a.keyframes.length) throw fail("bad_args", "keyframes: [{time, value}, …] is required");
const kfs = [];
for (const k of a.keyframes) {
const kf = param.createKeyframe(makeValue(k.value));
kf.position = await kfTime(clip, k.time, seq, a.timeBase);
kfs.push(kf);
}
const interp = a.interpolation ? { linear: ppro.Keyframe.INTERPOLATION_MODE_LINEAR, hold: ppro.Keyframe.INTERPOLATION_MODE_HOLD, bezier: ppro.Keyframe.INTERPOLATION_MODE_BEZIER }[a.interpolation] : undefined;
const animated = safe(() => param.isTimeVarying(), false);
tx(p, "Add Keyframes", () => {
const acts = [];
if (!animated) acts.push(param.createSetTimeVaryingAction(true));
for (const kf of kfs) acts.push(param.createAddKeyframeAction(kf));
if (interp !== undefined) for (const kf of kfs) acts.push(param.createSetInterpolationAtKeyframeAction(kf.position, interp, true));
return acts;
});
return { clip: ref, param: param.displayName, keyframes: a.keyframes.length };
},
async remove_keyframes(a) {
const { p, seq } = await seqCtx(a);
const { clip, ref } = await findClip(seq, a.clip);
const { comp } = await findComponent(clip, a.component);
const { param } = findParam(comp, a.param);
if (a.all) {
tx(p, "Clear Keyframes", () => param.createSetTimeVaryingAction(false));
return { clip: ref, cleared: true };
}
const s = await kfTime(clip, a.start !== undefined ? a.start : a.time, seq, a.timeBase);
const e = a.end !== undefined ? await kfTime(clip, a.end, seq, a.timeBase) : null;
tx(p, "Remove Keyframes", () => (e ? param.createRemoveKeyframeRangeAction(s, e, true) : param.createRemoveKeyframeAction(s, true)));
return { clip: ref, removed: true };
},
async set_transform(a) {
// Convenience over the built-in Motion / Opacity components.
const { seq } = await seqCtx(a);
const out = [];
const set = async (component, param, value) => {
if (value === undefined) return;
await handlers.set_param({ ...a, component, param, value });
out.push(`${component}.${param}=${JSON.stringify(value)}`);
};
await findClip(seq, a.clip);
await set("Motion", "Scale", a.scale);
await set("Motion", "Position", a.position);
await set("Motion", "Rotation", a.rotation);
await set("Motion", "Anchor Point", a.anchor);
await set("Opacity", "Opacity", a.opacity);
return { clip: a.clip, set: out };
},
async add_transition(a) {
const { p, seq } = await seqCtx(a);
const targets = await resolveClips(seq, a);
let match = a.matchName;
if (!match) {
const all = await ppro.TransitionFactory.getVideoTransitionMatchNames();
const q = String(a.name || "Cross Dissolve").toLowerCase().replace(/\s+/g, "");
match = all.find((m) => m.toLowerCase().replace(/\s+/g, "").includes(q));
if (!match) throw fail("transition_not_found", `No transition like '${a.name}'. premiere_list_effects {kind:"transition"} lists match names.`);
}
const opts = new ppro.AddTransitionOptions();
if (a.duration !== undefined) opts.setDuration(await toTick(a.duration, seq));
opts.setApplyToStart(a.position === "start");
if (a.singleSided) opts.setForceSingleSided(true);
if (a.alignment !== undefined) opts.setTransitionAlignment(Number(a.alignment));
const done = [];
for (const t of targets) {
if (t.kind !== "video") throw fail("video_only", "UXP only supports video transitions. For audio, use premiere_add_keyframes on Volume>Level to fade.");
const tr = ppro.TransitionFactory.createVideoTransition(match);
tx(p, "Add Transition", () => t.clip.createAddVideoTransitionAction(tr, opts));
done.push(t.ref);
}
return { transition: match, clips: done, position: a.position || "end" };
},
async remove_transition(a) {
const { p, seq } = await seqCtx(a);
const { clip, ref } = await findClip(seq, a.clip);
const pos = a.position === "start" ? C.TransitionPosition.START : C.TransitionPosition.END;
tx(p, "Remove Transition", () => clip.createRemoveVideoTransitionAction(pos));
return { clip: ref, removed: a.position || "end" };
},
async insert_mogrt(a) {
const { p, seq } = await seqCtx(a);
const t = await toTick(a.time || 0, seq);
const { index: v } = parseTrack(a.track || "V2");
const ed = ppro.SequenceEditor.getEditor(seq);
let items;
p.lockedAccess(() => {
items = a.path ? ed.insertMogrtFromPath(a.path, t, v, Number(a.audioTrack || 0)) : ed.insertMogrtFromLibrary(a.library, a.element, t, v, Number(a.audioTrack || 0));
});
return { inserted: (items || []).length, track: trackLabel("video", v), _note: "Edit its text with premiere_get_clip_effects then premiere_set_param on the Graphic's text param." };
},
// ── audio ──
async set_clip_volume(a) {
const { p, seq } = await seqCtx(a);
const targets = await resolveClips(seq, a);
const out = [];
for (const t of targets) {
if (t.kind !== "audio") throw fail("audio_only", `${t.ref} is a video clip — volume lives on the linked audio clip (A-track).`);
const param = await volumeParam(t.clip);
const level = a.level !== undefined ? Number(a.level) : dbToLevel(a.db === undefined ? 0 : a.db);
const kf = param.createKeyframe(level);
if (a.time !== undefined) {
kf.position = await kfTime(t.clip, a.time, seq, a.timeBase);
const animated = safe(() => param.isTimeVarying(), false);
tx(p, "Volume Keyframe", () => [!animated && param.createSetTimeVaryingAction(true), param.createAddKeyframeAction(kf)]);
} else {
tx(p, "Clip Volume", () => param.createSetValueAction(kf, true));
}
out.push({ clip: t.ref, db: a.db, level });
}
return { set: out };
},
async audio_fade(a) {
// Volume keyframes: fade in at clip start or fade out at clip end.
const { p, seq } = await seqCtx(a);
const { clip, ref, kind } = await findClip(seq, a.clip);
if (kind !== "audio") throw fail("audio_only", "Use an audio clip ref (A1:0)");
const param = await volumeParam(clip);
const dur = await toTick(a.duration || 1, seq);
// Media-time span of the clip (keyframes live in media time).
const start = await clip.getInPoint();
const end = start.add(await clip.getDuration());
const full = dbToLevel(a.db === undefined ? 0 : a.db);
const silent = dbToLevel(-96);
const pts = a.type === "out" ? [[end.subtract(dur), full], [end, silent]] : [[start, silent], [start.add(dur), full]];
const kfs = pts.map(([t, v]) => {
const k = param.createKeyframe(v);
k.position = t;
return k;
});
const animated = safe(() => param.isTimeVarying(), false);
tx(p, "Audio Fade", () => [!animated && param.createSetTimeVaryingAction(true), ...kfs.map((k) => param.createAddKeyframeAction(k))]);
return { clip: ref, fade: a.type || "in", seconds: round(dur.seconds) };
},
async audio_probe(a) {
const { seq } = await seqCtx(a);
const { clip, ref } = await findClip(seq, a.clip);
const param = await volumeParam(clip);
const v = await paramValueAt(param, a.time !== undefined ? await kfTime(clip, a.time, seq) : await clip.getInPoint());
return { clip: ref, levelRaw: v, dbEstimate: typeof v === "number" ? round(levelToDb(v)) : null };
},
// ── markers ──
async list_markers(a) {
const p = await project();
const owner = a.item ? ppro.ClipProjectItem.cast(await findItem(p, a.item)) : await findSequence(p, a.sequence);
const seq = a.item ? null : owner;
const m = await ppro.Markers.getMarkers(owner);
const out = [];
for (const k of m.getMarkers()) {
out.push({
guid: k.guid.toString(),
name: k.getName(),
comments: k.getComments(),
start: await timeOut(k.getStart(), seq),
duration: round(k.getDuration().seconds),
type: k.getType(),
colorIndex: k.getColorIndex(),
});
}
return { count: out.length, markers: out };
},
async add_marker(a) {
const p = await project();
const owner = a.item ? ppro.ClipProjectItem.cast(await findItem(p, a.item)) : await findSequence(p, a.sequence);
const seq = a.item ? null : owner;
const m = await ppro.Markers.getMarkers(owner);
const t = a.time !== undefined ? await toTick(a.time, seq) : seq ? await seq.getPlayerPosition() : ppro.TickTime.TIME_ZERO;
const d = a.duration !== undefined ? await toTick(a.duration, seq) : ppro.TickTime.TIME_ZERO;
const type = { comment: ppro.Marker.MARKER_TYPE_COMMENT, chapter: ppro.Marker.MARKER_TYPE_CHAPTER, weblink: ppro.Marker.MARKER_TYPE_WEBLINK }[a.type || "comment"] || ppro.Marker.MARKER_TYPE_COMMENT;
tx(p, "Add Marker", () => m.createAddMarkerAction(a.name || "", type, t, d, a.comments || ""));
if (a.colorIndex !== undefined) {
const m2 = await ppro.Markers.getMarkers(owner);
const added = m2.getMarkers().find((k) => k.getStart().ticks === t.ticks && k.getName() === (a.name || ""));
if (added) tx(p, "Marker Color", () => added.createSetColorByIndexAction(Number(a.colorIndex)));
}
return { added: a.name || "", at: await timeOut(t, seq) };
},
async remove_marker(a) {
const p = await project();
const owner = a.item ? ppro.ClipProjectItem.cast(await findItem(p, a.item)) : await findSequence(p, a.sequence);
const m = await ppro.Markers.getMarkers(owner);
const all = m.getMarkers();
const targets = a.all ? all : all.filter((k) => k.guid.toString() === a.guid || (a.name !== undefined && k.getName() === a.name));
if (!targets.length) throw fail("marker_not_found", "No matching marker (pass guid, name, or all:true)");
tx(p, "Remove Marker", () => targets.map((k) => m.createRemoveMarkerAction(k)));
return { removed: targets.length };
},
// ── preview & export ──
async export_frame(a) {
const { seq } = await seqCtx(a);
const t = a.time !== undefined ? await toTick(a.time, seq) : await seq.getPlayerPosition();
const size = await seq.getFrameSize();
let w = Number(a.width || 0);
let h = Number(a.height || 0);
if (!w && !h) w = Math.min(size.width, 1280);
if (w && !h) h = Math.round((size.height * w) / size.width);
if (h && !w) w = Math.round((size.width * h) / size.height);
const fmt = (a.format || "png").toLowerCase();
const dir = a.dir;
const base = a.name || `frame_${Math.round(t.seconds * 1000)}ms`;
if (!dir) throw fail("bad_args", "dir is required");
const ok = await ppro.Exporter.exportSequenceFrame(seq, t, `${base}.${fmt}`, dir, w, h);
if (!ok) throw fail("export_failed", `exportSequenceFrame failed (dir ${dir})`);
// Some builds append the extension again ("x.png.png"); report whichever file exists.
const sep = dir.includes("\\") ? "\\" : "/";
const stem = `${dir.replace(/[\\/]$/, "")}${sep}${base}.${fmt}`;
let path = stem;
for (const cand of [stem, `${stem}.${fmt}`]) {
try {
await fs.lstat(cand);
path = cand;
break;
} catch (_) {}
}
return { path, time: await timeOut(t, seq), width: w, height: h };
},
async export_frames(a) {
const { seq } = await seqCtx(a);
let times = a.times;
if (!times) {
const end = (await seq.getEndTime()).seconds;
const n = Math.max(1, Math.min(Number(a.count || 8), 64));
const from = a.start !== undefined ? (await toTick(a.start, seq)).seconds : 0;
const to = a.end !== undefined ? (await toTick(a.end, seq)).seconds : end;
times = Array.from({ length: n }, (_, i) => from + ((to - from) * (i + 0.5)) / n);
}
const frames = [];
for (let i = 0; i < times.length; i++) {
frames.push(await handlers.export_frame({ ...a, time: times[i], name: `${a.prefix || "frame"}_${String(i).padStart(3, "0")}` }));
}
return { frames };
},
async export_sequence(a) {
const { seq } = await seqCtx(a);
if (!a.output) throw fail("bad_args", "output (absolute file path on this machine) is required");
if (!a.preset) throw fail("bad_args", "preset (.epr path) is required — premiere_list_export_presets finds them");
const em = ppro.EncoderManager.getManager();
const mode = { immediate: C.ExportType.IMMEDIATELY, ame: C.ExportType.QUEUE_TO_AME, queue: C.ExportType.QUEUE_TO_APP }[a.mode || "immediate"];
if (a.mode === "ame" && !em.isAMEInstalled) throw fail("ame_missing", "Adobe Media Encoder isn't installed; use mode:\"immediate\".");
watchEncoder();
const ok = await em.exportSequence(seq, mode, a.output, a.preset, a.range !== "inout");
if (a.mode === "ame" && a.startQueue !== false) await em.startBatchEncode().catch(() => {});
return { started: ok, output: a.output, mode: a.mode || "immediate" };
},
async export_interchange(a) {
const { seq } = await seqCtx(a);
if (!a.output) throw fail("bad_args", "output path is required");
const f = a.format || "otio";
let ok;
if (f === "otio") ok = await ppro.ProjectConverter.exportAsOpenTimelineIO(seq, a.output, true);
else if (f === "fcpxml") ok = await ppro.ProjectConverter.exportAsFinalCutProXML(seq, a.output, true);
else if (f === "aaf") ok = await ppro.ProjectConverter.exportAAF(seq, a.output);
else throw fail("bad_args", "format must be otio, fcpxml, or aaf");
return { exported: ok, format: f, output: a.output };
},
async source_monitor(a) {
const p = await project();
if (a.close) return { closed: await ppro.SourceMonitor.closeAllClips() };
if (a.path) await ppro.SourceMonitor.openFilePath(a.path);
else if (a.item) await ppro.SourceMonitor.openProjectItem(await findItem(p, a.item));
if (a.time !== undefined) await ppro.SourceMonitor.setPosition(await toTick(a.time, null));
if (a.play) await ppro.SourceMonitor.play(Number(a.speed || 1));
const pi = await ppro.SourceMonitor.getProjectItem().catch(() => null);
return { item: pi ? pi.name : null, position: round((await ppro.SourceMonitor.getPosition()).seconds) };
},
// ── transcripts ──
async transcribe(a) {
const p = await project();
const clip = ppro.ClipProjectItem.cast(await findItem(p, a.item));
return { started: await ppro.Transcript.transcribeClipProjectItem(clip, a.language ? { language: a.language } : undefined) };
},
async get_transcript(a) {
const p = await project();
const clip = ppro.ClipProjectItem.cast(await findItem(p, a.item));
if (!ppro.Transcript.hasTranscript(clip)) throw fail("no_transcript", "No transcript yet — premiere_transcribe first (takes a while).");
const json = await ppro.Transcript.exportToJSON(clip);
return { transcript: safe(() => JSON.parse(json), json) };
},
// ── escape hatch ──
async eval(a) {
if (!a.code) throw fail("bad_args", "code is required: the body of an async function. In scope: ppro, h (helpers), args. `return` a JSON-able value.");
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
const fn = new AsyncFunction("ppro", "h", "args", "require", a.code);
const r = await fn(ppro, helpers, a.args || {}, require);
return { value: toJsonSafe(r) };
},
};
async function placeClip(a, mode) {
const { p, seq } = await seqCtx(a);
const item = await findItem(p, a.item);
const t = a.time !== undefined ? await toTick(a.time, seq) : await seq.getPlayerPosition();
const v = a.videoTrack !== undefined ? parseTrack(a.videoTrack).index : a.track ? parseTrack(a.track).index : 0;
const au = a.audioTrack !== undefined ? parseTrack(a.audioTrack).index : v;
const ed = ppro.SequenceEditor.getEditor(seq);
tx(p, mode === "insert" ? "Insert Clip" : "Overwrite Clip", () =>
mode === "insert" ? ed.createInsertProjectItemAction(item, t, v, au, !!a.limitShift) : ed.createOverwriteItemAction(item, t, v, au)
);
// Report the clip that now starts at t on the target video track (if any).
let placed = null;
try {
const track = await getTrack(seq, "video", v);
const clips = await trackClips(track);
for (let i = 0; i < clips.length; i++) {
if ((await clips[i].getStartTime()).ticks === t.ticks) {
placed = await describeClip(clips[i], seq, "video", v, i, false);
break;
}
}
} catch (_) {}
return { mode, item: item.name, at: await timeOut(t, seq), videoTrack: trackLabel("video", v), audioTrack: trackLabel("audio", au), placed };
}
async function clipStartingAt(track, t) {
for (const c of await trackClips(track)) {
if (Math.abs((await c.getStartTime()).ticksNumber - t.ticksNumber) < 1000) return c;
}
return null;
}
// ───────────────────────────── encoder events ─────────────────────────────
let encoderWatched = false;
function watchEncoder() {
if (encoderWatched) return;
encoderWatched = true;
try {
const em = ppro.EncoderManager.getManager();
const E = ppro.EncoderManager;
const on = (name, kind) =>
ppro.EventManager.addEventListener(em, name, (ev) => send({ type: "event", kind, data: toJsonSafe(ev), at: Date.now() }));
on(E.EVENT_RENDER_PROGRESS, "export_progress");
on(E.EVENT_RENDER_COMPLETE, "export_complete");
on(E.EVENT_RENDER_ERROR, "export_error");
on(E.EVENT_RENDER_CANCEL, "export_cancel");
on(E.EVENT_RENDER_QUEUE, "export_queued");
} catch (e) {
log("encoder events unavailable: " + e);
}
}
function watchProject() {
try {
const PE = ppro.ProjectEvent;
for (const [name, kind] of [[PE.EVENT_OPENED, "project_opened"], [PE.EVENT_ACTIVATED, "project_activated"], [PE.EVENT_DIRTY, "project_dirty"]]) {
ppro.EventManager.addGlobalEventListener(name, async () => {
send({ type: "event", kind, at: Date.now() });
send({ type: "hello_update", fields: await helloInfo() });
});
}
} catch (e) {
log("project events unavailable: " + e);
}
}
function toJsonSafe(v, depth = 0) {
if (v === null || v === undefined) return v === undefined ? null : v;
if (depth > 6) return "[…]";
if (typeof v !== "object") return typeof v === "function" ? "[function]" : v;
if (typeof v.ticks === "string" && typeof v.seconds === "number") return { seconds: v.seconds, ticks: v.ticks };
if (Array.isArray(v)) return v.map((x) => toJsonSafe(x, depth + 1));
const out = {};
for (const k of Object.keys(v)) {
try {
out[k] = toJsonSafe(v[k], depth + 1);
} catch (_) {}
}
if (!Object.keys(out).length && typeof v.toString === "function" && v.toString !== Object.prototype.toString) return v.toString();
return out;
}
// Exposed to premiere_eval as `h`.
const helpers = { project, findSequence, findItem, findClip, getTrack, trackClips, toTick, timeOut, tx, componentsOf, findComponent, findParam, dbToLevel, levelToDb, allItems, parseTrack, handlers };
// ───────────────────────────── boot ─────────────────────────────
watchProject();
connect();