- Adicionado estrutura completa do projeto - Configurado MCP server para Premiere Pro - Adicionado documentação e skills - Configurado Gitignore para o projeto
1728 lines
113 KiB
JavaScript
Executable File
1728 lines
113 KiB
JavaScript
Executable File
(function (root, factory) {
|
|
const api = factory();
|
|
if (typeof module === "object" && module.exports) module.exports = api;
|
|
root.PremiereMcpCommands = api;
|
|
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
|
|
"use strict";
|
|
|
|
function createCommandRegistry(deps) {
|
|
const ppro = deps.ppro, Protocol = deps.Protocol, workspace = deps.workspace;
|
|
const completedOperations = new Map();
|
|
const inFlightOperations = new Map();
|
|
const sequenceRangeUpdateTails = new Map();
|
|
const transitionUpdateTails = new Map();
|
|
const sequencePlayheadSetTails = new Map();
|
|
const appPreferenceSetTails = new Map();
|
|
const listedVideoTransitionMatchNames = new Set();
|
|
const APP_PREFERENCE_KEYS = Object.freeze({
|
|
auto_peak_generation: "KEY_AUTO_PEAK_GENERATION",
|
|
import_workspace: "KEY_IMPORT_WORKSPACE",
|
|
show_quickstart_dialog: "KEY_SHOW_QUICKSTART_DIALOG"
|
|
});
|
|
const MAX_APP_PREFERENCE_VALUE_CHARS = 1024;
|
|
// Adobe's current Sequence Settings reference caps a sequence at 10,240 x
|
|
// 8,192 pixels. These bounds make the read-only bridge reject malformed
|
|
// host data instead of serializing an implausible frame size.
|
|
const MAX_SEQUENCE_FRAME_WIDTH = 10240;
|
|
const MAX_SEQUENCE_FRAME_HEIGHT = 8192;
|
|
const MAX_TIME_ALIGNMENT_SECONDS = 86400;
|
|
const MAX_TIME_ALIGNMENT_FRAME_COUNT = MAX_TIME_ALIGNMENT_SECONDS * 240;
|
|
// Preserve the native decimal ticks value as a string. Eighteen digits is a
|
|
// generous bounded protocol representation while avoiding an unbounded
|
|
// value from the host.
|
|
const MAX_SEQUENCE_TIMEBASE_DIGITS = 18;
|
|
const definitions = {
|
|
"capabilities.get": { readOnly: true, handler: capabilities },
|
|
"state.get": { readOnly: true, handler: stateSnapshot },
|
|
"project.snapshot": { readOnly: true, minHostVersion: "25.6.0", probe: canInspectProject, handler: projectSnapshot },
|
|
"project.insertionBin.inspect": { readOnly: true, minHostVersion: "25.6.0", probe: canInspectProjectInsertionBin, handler: inspectProjectInsertionBin },
|
|
"graphics.mogrtPath.inspect": { readOnly: true, minHostVersion: "25.6.0", probe: canInspectInstalledMogrtDirectory, handler: inspectInstalledMogrtDirectory },
|
|
"project.save": { idempotent: true, minHostVersion: "25.6.0", probe: canSaveProject, handler: saveProject },
|
|
"sequence.createPreset": { destructive: true, undoable: false, requiresWorkspace: true, minHostVersion: "26.3.0", probe: canCreatePresetSequence, handler: createPresetSequence },
|
|
"sequence.range.inspect": { readOnly: true, minHostVersion: "25.6.0", probe: canInspectSequenceRange, handler: inspectSequenceRange },
|
|
"sequence.range.update": { destructive: true, undoable: true, idempotent: true, minHostVersion: "25.6.0", probe: canUpdateSequenceRange, handler: updateSequenceRange },
|
|
"sequence.playhead.inspect": { readOnly: true, minHostVersion: "25.6.0", probe: canInspectSequencePlayhead, handler: inspectSequencePlayhead },
|
|
"sequence.playhead.set": { idempotent: true, minHostVersion: "25.6.0", probe: canSetSequencePlayhead, handler: setSequencePlayhead },
|
|
"time.frameAlignment.inspect": { readOnly: true, minHostVersion: "25.6.0", probe: canInspectFrameAlignment, handler: inspectFrameAlignment },
|
|
"sequence.timing.inspect": { readOnly: true, minHostVersion: "25.6.0", probe: canInspectSequenceTiming, handler: inspectSequenceTiming },
|
|
"sequence.timingByGuid.inspect": { readOnly: true, targetCapabilityProbe: true, minHostVersion: "25.6.0", probe: canInspectSequenceTimingByGuid, handler: inspectSequenceTimingByGuid },
|
|
"preferences.inspect": { readOnly: true, minHostVersion: "25.6.0", probe: canInspectAppPreferences, handler: inspectAppPreferences },
|
|
"preferences.set": { idempotent: true, minHostVersion: "25.6.0", probe: canSetAppPreferences, handler: setAppPreference },
|
|
"interchange.export": { requiresWorkspace: true, minHostVersion: "26.2.0", probe: canExportInterchange, handler: exportInterchange },
|
|
"interchange.aaf.export": { destructive: true, undoable: false, idempotent: true, requiresWorkspace: true, minHostVersion: "26.3.0", probe: canExportAaf, handler: exportAaf },
|
|
"track.rename": { destructive: true, undoable: true, idempotent: true, minHostVersion: "26.3.0", probe: canRenameTracks, handler: renameTrack },
|
|
"subclip.create": { destructive: true, undoable: true, minHostVersion: "26.3.0", probe: canCreateSubclips, handler: createSubclip },
|
|
"marker.list": { readOnly: true, minHostVersion: "26.3.0", probe: canListMarkers, handler: listMarkers },
|
|
"sourceMonitor.position.set": { idempotent: true, minHostVersion: "26.3.0", probe: canSetSourceMonitorPosition, handler: setSourceMonitorPosition },
|
|
"transcript.languages": { readOnly: true, minHostVersion: "26.3.0", probe: canQueryTranscriptLanguages, handler: transcriptLanguages },
|
|
"objectMask.has": { readOnly: true, minHostVersion: "26.3.0", probe: canInspectObjectMasks, handler: hasObjectMask },
|
|
"encoder.configure": { minHostVersion: "26.3.0", probe: canConfigureEncoder, handler: configureEncoder },
|
|
"frame.export": { requiresWorkspace: true, minHostVersion: "25.6.0", probe: canExportFrame, handler: exportFrame },
|
|
"timeline.selection.lift": { destructive: true, undoable: true, minHostVersion: "25.6.0", probe: canLiftSelection, handler: liftSelection },
|
|
"transition.video.list": { readOnly: true, minHostVersion: "25.6.0", probe: canListTransitions, handler: listTransitions },
|
|
"transition.video.inspect": { readOnly: true, targetCapabilityProbe: true, minHostVersion: "25.6.0", probe: canInspectTransitions, handler: inspectTransition },
|
|
"transition.video.add": { destructive: true, undoable: true, idempotent: true, targetCapabilityProbe: true, minHostVersion: "25.6.0", probe: canMutateTransitions, handler: addTransition },
|
|
"transition.video.remove": { destructive: true, undoable: true, idempotent: true, targetCapabilityProbe: true, minHostVersion: "25.6.0", probe: canMutateTransitions, handler: removeTransition }
|
|
};
|
|
let workflowApi = deps.Workflows || (typeof globalThis !== "undefined" && globalThis.PremiereMcpWorkflows);
|
|
if (!workflowApi && typeof require === "function") workflowApi = require("./workflows.cjs");
|
|
if (workflowApi && typeof workflowApi.createWorkflowDefinitions === "function") {
|
|
Object.assign(definitions, workflowApi.createWorkflowDefinitions({ ppro, Protocol, workspace }));
|
|
}
|
|
let projectItemColorLabelLocksApi = deps.ProjectItemColorLabelLocks || (typeof globalThis !== "undefined" && globalThis.PremiereMcpProjectItemColorLabelLocks);
|
|
if (!projectItemColorLabelLocksApi && typeof require === "function") projectItemColorLabelLocksApi = require("./project-item-color-label-locks.cjs");
|
|
const projectItemColorLabelLocks = projectItemColorLabelLocksApi && typeof projectItemColorLabelLocksApi.createProjectItemColorLabelLocks === "function"
|
|
? projectItemColorLabelLocksApi.createProjectItemColorLabelLocks()
|
|
: null;
|
|
let advancedWorkflowApi = deps.AdvancedWorkflows || (typeof globalThis !== "undefined" && globalThis.PremiereMcpAdvancedWorkflows);
|
|
if (!advancedWorkflowApi && typeof require === "function") advancedWorkflowApi = require("./advanced-workflows.cjs");
|
|
if (advancedWorkflowApi && typeof advancedWorkflowApi.createAdvancedWorkflowDefinitions === "function") {
|
|
Object.assign(definitions, advancedWorkflowApi.createAdvancedWorkflowDefinitions({
|
|
ppro, Protocol, workspace, events: deps.events, colorLabelLocks: projectItemColorLabelLocks
|
|
}));
|
|
}
|
|
let dialogueWorkflowApi = deps.DialogueWorkflows || (typeof globalThis !== "undefined" && globalThis.PremiereMcpDialogueWorkflows);
|
|
if (!dialogueWorkflowApi && typeof require === "function") dialogueWorkflowApi = require("./dialogue-workflows.cjs");
|
|
if (dialogueWorkflowApi && typeof dialogueWorkflowApi.createDialogueWorkflowDefinitions === "function") {
|
|
Object.assign(definitions, dialogueWorkflowApi.createDialogueWorkflowDefinitions({ ppro }));
|
|
}
|
|
let timelineSourceLabelWorkflowApi = deps.TimelineSourceLabelWorkflows || (typeof globalThis !== "undefined" && globalThis.PremiereMcpTimelineSourceLabelWorkflows);
|
|
if (!timelineSourceLabelWorkflowApi && typeof require === "function") timelineSourceLabelWorkflowApi = require("./timeline-source-label-workflows.cjs");
|
|
if (timelineSourceLabelWorkflowApi && typeof timelineSourceLabelWorkflowApi.createTimelineSourceLabelWorkflowDefinitions === "function") {
|
|
Object.assign(definitions, timelineSourceLabelWorkflowApi.createTimelineSourceLabelWorkflowDefinitions({
|
|
ppro, colorLabelLocks: projectItemColorLabelLocks
|
|
}));
|
|
}
|
|
let sequencePreviewFrameWorkflowApi = deps.SequencePreviewFrameWorkflows || (typeof globalThis !== "undefined" && globalThis.PremiereMcpSequencePreviewFrameWorkflows);
|
|
if (!sequencePreviewFrameWorkflowApi && typeof require === "function") sequencePreviewFrameWorkflowApi = require("./sequence-preview-frame-workflows.cjs");
|
|
if (sequencePreviewFrameWorkflowApi && typeof sequencePreviewFrameWorkflowApi.createSequencePreviewFrameWorkflowDefinitions === "function") {
|
|
Object.assign(definitions, sequencePreviewFrameWorkflowApi.createSequencePreviewFrameWorkflowDefinitions({ ppro }));
|
|
}
|
|
let sourceMediaProvenanceWorkflowApi = deps.SourceMediaProvenanceWorkflows || (typeof globalThis !== "undefined" && globalThis.PremiereMcpSourceMediaProvenanceWorkflows);
|
|
if (!sourceMediaProvenanceWorkflowApi && typeof require === "function") sourceMediaProvenanceWorkflowApi = require("./source-media-provenance-workflows.cjs");
|
|
if (sourceMediaProvenanceWorkflowApi && typeof sourceMediaProvenanceWorkflowApi.createSourceMediaProvenanceWorkflowDefinitions === "function") {
|
|
Object.assign(definitions, sourceMediaProvenanceWorkflowApi.createSourceMediaProvenanceWorkflowDefinitions({ ppro }));
|
|
}
|
|
let sourceProxyWorkflowApi = deps.SourceProxyWorkflows || (typeof globalThis !== "undefined" && globalThis.PremiereMcpSourceProxyWorkflows);
|
|
if (!sourceProxyWorkflowApi && typeof require === "function") sourceProxyWorkflowApi = require("./source-proxy-workflows.cjs");
|
|
if (sourceProxyWorkflowApi && typeof sourceProxyWorkflowApi.createSourceProxyWorkflowDefinitions === "function") {
|
|
Object.assign(definitions, sourceProxyWorkflowApi.createSourceProxyWorkflowDefinitions({ ppro }));
|
|
}
|
|
let tickTimeArithmeticWorkflowApi = deps.TickTimeArithmeticWorkflows || (typeof globalThis !== "undefined" && globalThis.PremiereMcpTickTimeArithmeticWorkflows);
|
|
if (!tickTimeArithmeticWorkflowApi && typeof require === "function") tickTimeArithmeticWorkflowApi = require("./tick-time-arithmetic-workflows.cjs");
|
|
if (tickTimeArithmeticWorkflowApi && typeof tickTimeArithmeticWorkflowApi.createTickTimeArithmeticWorkflowDefinitions === "function") {
|
|
Object.assign(definitions, tickTimeArithmeticWorkflowApi.createTickTimeArithmeticWorkflowDefinitions({ ppro }));
|
|
}
|
|
let objectMaskAuditWorkflowApi = deps.ObjectMaskAuditWorkflows || (typeof globalThis !== "undefined" && globalThis.PremiereMcpObjectMaskAuditWorkflows);
|
|
if (!objectMaskAuditWorkflowApi && typeof require === "function") objectMaskAuditWorkflowApi = require("./object-mask-audit-workflows.cjs");
|
|
if (objectMaskAuditWorkflowApi && typeof objectMaskAuditWorkflowApi.createObjectMaskAuditWorkflowDefinitions === "function") {
|
|
Object.assign(definitions, objectMaskAuditWorkflowApi.createObjectMaskAuditWorkflowDefinitions({ ppro }));
|
|
}
|
|
let uniqueIdentityWorkflowApi = deps.UniqueIdentityWorkflows || (typeof globalThis !== "undefined" && globalThis.PremiereMcpUniqueIdentityWorkflows);
|
|
if (!uniqueIdentityWorkflowApi && typeof require === "function") uniqueIdentityWorkflowApi = require("./unique-identity-workflows.cjs");
|
|
if (uniqueIdentityWorkflowApi && typeof uniqueIdentityWorkflowApi.createUniqueIdentityWorkflowDefinitions === "function") {
|
|
Object.assign(definitions, uniqueIdentityWorkflowApi.createUniqueIdentityWorkflowDefinitions({ ppro }));
|
|
}
|
|
let effectParameterCatalogWorkflowApi = deps.EffectParameterCatalogWorkflows || (typeof globalThis !== "undefined" && globalThis.PremiereMcpEffectParameterCatalogWorkflows);
|
|
if (!effectParameterCatalogWorkflowApi && typeof require === "function") effectParameterCatalogWorkflowApi = require("./effect-parameter-catalog-workflows.cjs");
|
|
if (effectParameterCatalogWorkflowApi && typeof effectParameterCatalogWorkflowApi.createEffectParameterCatalogWorkflowDefinitions === "function") {
|
|
Object.assign(definitions, effectParameterCatalogWorkflowApi.createEffectParameterCatalogWorkflowDefinitions({ ppro }));
|
|
}
|
|
let trackItemLocksApi = deps.TrackItemMutationLocks || (typeof globalThis !== "undefined" && globalThis.PremiereMcpTrackItemMutationLocks);
|
|
if (!trackItemLocksApi && typeof require === "function") trackItemLocksApi = require("./track-item-mutation-locks.cjs");
|
|
const trackItemLocks = trackItemLocksApi && typeof trackItemLocksApi.createTrackItemMutationLocks === "function"
|
|
? trackItemLocksApi.createTrackItemMutationLocks()
|
|
: null;
|
|
let slipWorkflowApi = deps.SlipWorkflows || (typeof globalThis !== "undefined" && globalThis.PremiereMcpSlipWorkflows);
|
|
if (!slipWorkflowApi && typeof require === "function") slipWorkflowApi = require("./slip-workflows.cjs");
|
|
if (slipWorkflowApi && typeof slipWorkflowApi.createSlipWorkflowDefinitions === "function") {
|
|
Object.assign(definitions, slipWorkflowApi.createSlipWorkflowDefinitions({ ppro, locks: trackItemLocks }));
|
|
}
|
|
let slideWorkflowApi = deps.SlideWorkflows || (typeof globalThis !== "undefined" && globalThis.PremiereMcpSlideWorkflows);
|
|
if (!slideWorkflowApi && typeof require === "function") slideWorkflowApi = require("./slide-workflows.cjs");
|
|
if (slideWorkflowApi && typeof slideWorkflowApi.createSlideWorkflowDefinitions === "function") {
|
|
Object.assign(definitions, slideWorkflowApi.createSlideWorkflowDefinitions({ ppro, locks: trackItemLocks }));
|
|
}
|
|
let cloneWorkflowApi = deps.CloneWorkflows || (typeof globalThis !== "undefined" && globalThis.PremiereMcpCloneWorkflows);
|
|
if (!cloneWorkflowApi && typeof require === "function") cloneWorkflowApi = require("./clone-workflows.cjs");
|
|
if (cloneWorkflowApi && typeof cloneWorkflowApi.createCloneWorkflowDefinitions === "function") {
|
|
Object.assign(definitions, cloneWorkflowApi.createCloneWorkflowDefinitions({ ppro, locks: trackItemLocks }));
|
|
}
|
|
let rippleDeleteWorkflowApi = deps.RippleDeleteWorkflows || (typeof globalThis !== "undefined" && globalThis.PremiereMcpRippleDeleteWorkflows);
|
|
if (!rippleDeleteWorkflowApi && typeof require === "function") rippleDeleteWorkflowApi = require("./ripple-delete-workflows.cjs");
|
|
if (rippleDeleteWorkflowApi && typeof rippleDeleteWorkflowApi.createRippleDeleteWorkflowDefinitions === "function") {
|
|
Object.assign(definitions, rippleDeleteWorkflowApi.createRippleDeleteWorkflowDefinitions({ ppro, locks: trackItemLocks }));
|
|
}
|
|
let nextWorkflowApi = deps.NextWorkflows || (typeof globalThis !== "undefined" && globalThis.PremiereMcpNextWorkflows);
|
|
if (!nextWorkflowApi && typeof require === "function") nextWorkflowApi = require("./next-workflows.cjs");
|
|
let nextWorkflowRuntime = null;
|
|
if (nextWorkflowApi && typeof nextWorkflowApi.createNextWorkflowRuntime === "function") {
|
|
nextWorkflowRuntime = nextWorkflowApi.createNextWorkflowRuntime({
|
|
ppro, Protocol, workspace, events: deps.events, storage: deps.storage,
|
|
now: deps.now, sleep: deps.sleep, setTimer: deps.setTimer, clearTimer: deps.clearTimer
|
|
});
|
|
Object.assign(definitions, nextWorkflowRuntime.definitions);
|
|
} else if (nextWorkflowApi && typeof nextWorkflowApi.createNextWorkflowDefinitions === "function") {
|
|
Object.assign(definitions, nextWorkflowApi.createNextWorkflowDefinitions({
|
|
ppro, Protocol, workspace, events: deps.events
|
|
}));
|
|
}
|
|
if (typeof deps.transcriptImportHandler === "function") {
|
|
definitions["transcript.import"] = {
|
|
destructive: true,
|
|
undoable: true,
|
|
idempotent: true,
|
|
targetCapabilityProbe: true,
|
|
minHostVersion: "26.3.0",
|
|
probe: typeof deps.transcriptImportProbe === "function" ? deps.transcriptImportProbe : null,
|
|
handler: deps.transcriptImportHandler
|
|
};
|
|
}
|
|
|
|
async function dispatch(command, args) {
|
|
const definition = definitions[command];
|
|
if (!definition) throw commandError("UXP_UNSUPPORTED_COMMAND", "Unsupported UXP command: " + command);
|
|
if (definition.probe && !await definition.probe()) throw commandError("UXP_COMMAND_UNAVAILABLE", command + " is not supported by this Premiere build");
|
|
const input = args || {};
|
|
const operationId = validateOperationId(input.operationId);
|
|
const operationKey = operationId ? command + ":" + operationId : null;
|
|
if (!definition.readOnly && operationKey && completedOperations.has(operationKey)) {
|
|
return { ...completedOperations.get(operationKey), replayed: true };
|
|
}
|
|
if (!definition.readOnly && operationKey && inFlightOperations.has(operationKey)) {
|
|
return { ...await inFlightOperations.get(operationKey), replayed: true };
|
|
}
|
|
const execute = async () => {
|
|
const result = await definition.handler(input);
|
|
return definition.readOnly ? result : {
|
|
operationId: operationId || null,
|
|
outcome: result && result.outcome ? result.outcome : "verified",
|
|
...result
|
|
};
|
|
};
|
|
if (definition.readOnly || !operationKey) return execute();
|
|
|
|
// Reserve the idempotency key before the handler starts so concurrent
|
|
// WebSocket dispatches share one host mutation instead of racing it.
|
|
const pending = Promise.resolve().then(execute);
|
|
inFlightOperations.set(operationKey, pending);
|
|
try {
|
|
const envelope = await pending;
|
|
completedOperations.set(operationKey, envelope);
|
|
if (completedOperations.size > 256) completedOperations.delete(completedOperations.keys().next().value);
|
|
return envelope;
|
|
} finally {
|
|
if (inFlightOperations.get(operationKey) === pending) inFlightOperations.delete(operationKey);
|
|
}
|
|
}
|
|
async function capabilities() {
|
|
let project = null, sequence = null;
|
|
try { project = await ppro.Project.getActiveProject(); sequence = project && await project.getActiveSequence(); } catch (_) {}
|
|
const workspaceState = workspace && typeof workspace.status === "function" ? workspace.status() : {
|
|
configured: false, accessMode: "unavailable", rootName: null, persistent: false,
|
|
pathDisclosure: "redacted", canonicalPathValidation: "unavailable"
|
|
};
|
|
const commands = {};
|
|
for (const name of Object.keys(definitions)) {
|
|
const definition = definitions[name], apiSupported = !definition.probe || await definition.probe();
|
|
const pathValidationSupported = !definition.requiresWorkspace || workspaceState.canonicalPathValidation === "available";
|
|
const supported = apiSupported && pathValidationSupported;
|
|
commands[name] = {
|
|
supported, backend: "uxp", documented: true,
|
|
readOnly: !!definition.readOnly, destructive: !!definition.destructive,
|
|
undoable: !!definition.undoable, idempotent: !!definition.idempotent
|
|
};
|
|
if (definition.minHostVersion) commands[name].minHostVersion = definition.minHostVersion;
|
|
if (definition.requiresWorkspace) commands[name].workspaceRequired = true;
|
|
if (definition.conditionalWorkspace) commands[name].workspaceRequired = "path_variant_only";
|
|
if (definition.targetCapabilityProbe) commands[name].targetCapabilityProbe = "invocation";
|
|
if (!apiSupported) commands[name].reason = "Required Premiere UXP API is unavailable in this host";
|
|
else if (!pathValidationSupported) commands[name].reason = "This UXP host cannot canonically validate native paths; use the CEP fallback for path-based workflows";
|
|
}
|
|
return {
|
|
backend: "uxp", protocolVersion: Protocol.PROTOCOL_VERSION, hostMinVersion: "25.6.0",
|
|
activeProject: !!project, activeSequence: !!sequence, workspace: workspaceState, commands,
|
|
fallback: { backend: "cep", reason: "Use CEP/QE only when a command is absent or reports unsupported; never silently retry a failed UXP mutation." }
|
|
};
|
|
}
|
|
async function stateSnapshot() {
|
|
const project = await ppro.Project.getActiveProject();
|
|
const sequence = project && await project.getActiveSequence();
|
|
const position = sequence && await sequence.getPlayerPosition();
|
|
return { projectOpen: !!project, sequenceOpen: !!sequence, playheadSeconds: position ? position.seconds : null };
|
|
}
|
|
async function projectSnapshot() {
|
|
const project = await ppro.Project.getActiveProject();
|
|
if (!project) return { revision: "no-project", project: null, sequences: [] };
|
|
const sequences = Array.from(await project.getSequences() || []).map((sequence) => ({
|
|
guid: String(sequence.guid || ""), name: String(sequence.name || "")
|
|
}));
|
|
const active = await project.getActiveSequence();
|
|
const revision = simpleRevision([project.guid, project.name, project.path, active && active.guid, sequences.map((item) => item.guid).join(",")].join("|"));
|
|
return {
|
|
revision,
|
|
project: { guid: String(project.guid || ""), name: String(project.name || ""), path: String(project.path || "") },
|
|
activeSequenceGuid: active ? String(active.guid || "") : null,
|
|
sequences
|
|
};
|
|
}
|
|
async function inspectProjectInsertionBin(args) {
|
|
assertOnlyKeys(args, []);
|
|
const project = await ppro.Project.getActiveProject();
|
|
if (!project) throw commandError("UXP_NO_ACTIVE_PROJECT", "No active project");
|
|
const projectGuid = requiredProjectGuid(project);
|
|
const before = await insertionBinSnapshot(project);
|
|
// The insertion target is panel state that can change while its item
|
|
// properties are read. Resolve both the active project and target again
|
|
// before returning so callers never receive a mixed-project snapshot.
|
|
const currentProject = await ppro.Project.getActiveProject();
|
|
if (!currentProject || requiredProjectGuid(currentProject) !== projectGuid) {
|
|
throw commandError("UXP_STALE_PROJECT", "The active project changed while reading the insertion bin; retry the inspection");
|
|
}
|
|
const insertionBin = await insertionBinSnapshot(currentProject);
|
|
if (before.projectItemId !== insertionBin.projectItemId) {
|
|
throw commandError("UXP_STALE_INSERTION_BIN", "The Project-panel insertion bin changed while reading it; retry the inspection");
|
|
}
|
|
return { projectGuid, insertionBin, verificationBoundary: "project_insertion_bin_identity_readback" };
|
|
}
|
|
async function inspectInstalledMogrtDirectory(args) {
|
|
assertOnlyKeys(args, ["includePath"]);
|
|
const includePath = optionalBoolean(args.includePath, false, "includePath");
|
|
const path = await ppro.SequenceEditor.getInstalledMogrtPath();
|
|
// This bridge deliberately does not turn the returned host path into a
|
|
// filesystem capability. It only validates the bounded native value, and
|
|
// returns it when the caller explicitly requested disclosure.
|
|
if (typeof path !== "string" || !path.trim() || path.length > 4096 || path.includes("\0")) {
|
|
throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not return a valid installed MOGRT directory path");
|
|
}
|
|
return {
|
|
available: true,
|
|
installedMogrtPath: includePath ? path : null,
|
|
pathDisclosure: includePath ? "requested" : "redacted",
|
|
verificationBoundary: "documented_sequence_editor_installation_directory_readback"
|
|
};
|
|
}
|
|
async function insertionBinSnapshot(project) {
|
|
if (typeof project.getInsertionBin !== "function") {
|
|
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere does not expose the Project-panel insertion bin");
|
|
}
|
|
const item = await project.getInsertionBin();
|
|
if (!item || typeof item.getId !== "function") {
|
|
throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not return an identifiable insertion bin");
|
|
}
|
|
const projectItemId = await item.getId();
|
|
if (typeof projectItemId !== "string" || !projectItemId.trim() || projectItemId.length > 512) {
|
|
throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not return a valid insertion-bin project-item ID");
|
|
}
|
|
const name = String(item.name == null ? "" : item.name);
|
|
if (name.length > 1024) throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an oversized insertion-bin name");
|
|
const type = Number(item.type);
|
|
if (!Number.isInteger(type) || type < 0 || type > 1024) {
|
|
throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not return a valid insertion-bin item type");
|
|
}
|
|
return { projectItemId, name, type };
|
|
}
|
|
function requiredProjectGuid(project) {
|
|
const guid = stringifyGuid(project && project.guid);
|
|
if (!guid || guid.length > 512) throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not return a valid active-project GUID");
|
|
return guid;
|
|
}
|
|
async function saveProject() {
|
|
const project = await ppro.Project.getActiveProject();
|
|
if (!project) throw commandError("UXP_NO_ACTIVE_PROJECT", "No active project");
|
|
const saved = await project.save();
|
|
if (!saved) throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not confirm the project save");
|
|
return { saved: true, outcome: "verified", projectGuid: String(project.guid || "") };
|
|
}
|
|
async function createPresetSequence(args) {
|
|
assertOnlyKeys(args, ["name", "presetPath", "operationId"]);
|
|
const name = requiredString(args.name, "name"), presetPath = await allowedPath(args.presetPath, "presetPath", "file");
|
|
const project = await ppro.Project.getActiveProject();
|
|
if (!project) throw commandError("UXP_NO_ACTIVE_PROJECT", "No active project");
|
|
const sequence = await project.createSequenceWithPresetPath(name, presetPath);
|
|
if (!sequence) throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not return the created sequence");
|
|
const verified = Array.from(await project.getSequences() || []).some((item) => String(item.guid || "") === String(sequence.guid || ""));
|
|
if (!verified) throw commandError("UXP_VERIFICATION_FAILED", "Created sequence was not present in the project");
|
|
return { created: true, outcome: "verified", sequence: { guid: String(sequence.guid || ""), name: String(sequence.name || name) } };
|
|
}
|
|
async function inspectSequenceRange(args) {
|
|
assertOnlyKeys(args, []);
|
|
const context = await activeContext(false);
|
|
return {
|
|
...await sequenceRangeSnapshot(context.sequence),
|
|
verificationBoundary: "sequence_range_readback"
|
|
};
|
|
}
|
|
async function updateSequenceRange(args) {
|
|
const input = validateSequenceRangeUpdateArgs(args);
|
|
// TickTime construction can cross the host boundary. Do it before the
|
|
// per-sequence exclusion scope so the guarded snapshot is the final
|
|
// asynchronous preflight step before action creation.
|
|
const ticks = {
|
|
inPoint: input.updates.inSeconds == null ? null : await tickTime(input.updates.inSeconds, "updates.inSeconds"),
|
|
outPoint: input.updates.outSeconds == null ? null : await tickTime(input.updates.outSeconds, "updates.outSeconds"),
|
|
zeroPoint: input.updates.zeroPointSeconds == null ? null : await tickTime(input.updates.zeroPointSeconds, "updates.zeroPointSeconds")
|
|
};
|
|
return withSequenceRangeUpdateLock(input.expectedSequenceGuid, async () => {
|
|
const context = await activeContext(true);
|
|
const before = await sequenceRangeSnapshot(context.sequence);
|
|
if (before.sequenceGuid !== input.expectedSequenceGuid) {
|
|
throw commandError("UXP_STALE_SEQUENCE", "The active sequence changed before the range update; inspect the current range and retry");
|
|
}
|
|
assertExpectedSequenceRange(before.range, input.expectedRange);
|
|
const desired = { ...before.range, ...input.updates };
|
|
assertValidSequenceRange(desired, "requested sequence range");
|
|
let committed = false;
|
|
context.project.lockedAccess(() => {
|
|
committed = context.project.executeTransaction((compoundAction) => {
|
|
if (ticks.inPoint) {
|
|
const action = context.sequence.createSetInPointAction(ticks.inPoint);
|
|
if (!action || compoundAction.addAction(action) === false) {
|
|
throw commandError("UXP_ACTION_REJECTED", "Premiere rejected the sequence in point action");
|
|
}
|
|
}
|
|
if (ticks.outPoint) {
|
|
const action = context.sequence.createSetOutPointAction(ticks.outPoint);
|
|
if (!action || compoundAction.addAction(action) === false) {
|
|
throw commandError("UXP_ACTION_REJECTED", "Premiere rejected the sequence out point action");
|
|
}
|
|
}
|
|
if (ticks.zeroPoint) {
|
|
const action = context.sequence.createSetZeroPointAction(ticks.zeroPoint);
|
|
if (!action || compoundAction.addAction(action) === false) {
|
|
throw commandError("UXP_ACTION_REJECTED", "Premiere rejected the sequence zero point action");
|
|
}
|
|
}
|
|
}, "Update sequence range");
|
|
});
|
|
assertTransactionCommitted(committed, "sequence range update");
|
|
const after = await sequenceRangeSnapshot(context.sequence);
|
|
if (after.sequenceGuid !== before.sequenceGuid || !sameSequenceRange(after.range, desired)) {
|
|
throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not retain the requested sequence range");
|
|
}
|
|
return {
|
|
updated: true,
|
|
outcome: "verified",
|
|
sequenceGuid: after.sequenceGuid,
|
|
range: after.range,
|
|
verified: "sequence_range_readback",
|
|
operation: operationSemantics({
|
|
mutatesProject: true,
|
|
verificationStatus: "verified",
|
|
verificationBoundary: "sequence_range_readback",
|
|
verificationEvidence: [{ type: "sequence_range", sequenceGuid: after.sequenceGuid, range: after.range }],
|
|
undoSupported: true,
|
|
undoLabel: "Update sequence range",
|
|
transactionActionGroup: true,
|
|
cancellationSupported: false
|
|
})
|
|
};
|
|
});
|
|
}
|
|
function withSequenceRangeUpdateLock(sequenceGuid, operation) {
|
|
const previous = sequenceRangeUpdateTails.get(sequenceGuid) || Promise.resolve();
|
|
let release;
|
|
const gate = new Promise((resolve) => { release = resolve; });
|
|
const tail = previous.catch(() => undefined).then(() => gate);
|
|
sequenceRangeUpdateTails.set(sequenceGuid, tail);
|
|
return previous.catch(() => undefined).then(operation).finally(() => {
|
|
release();
|
|
if (sequenceRangeUpdateTails.get(sequenceGuid) === tail) sequenceRangeUpdateTails.delete(sequenceGuid);
|
|
});
|
|
}
|
|
async function inspectSequencePlayhead(args) {
|
|
assertOnlyKeys(args, []);
|
|
const context = await activeContext(false);
|
|
return {
|
|
...await sequencePlayheadSnapshot(context.sequence),
|
|
verificationBoundary: "sequence_playhead_readback"
|
|
};
|
|
}
|
|
function inspectFrameAlignment(args) {
|
|
assertObject(args);
|
|
assertOnlyKeys(args, ["action", "frameRate", "seconds", "frameCount"]);
|
|
const action = args.action;
|
|
if (action !== "align" && action !== "frame") {
|
|
throw commandError("UXP_INVALID_ARGUMENT", "action must be align or frame");
|
|
}
|
|
const requestedFrameRate = boundedFiniteNumber(args.frameRate, "frameRate", 1, 240);
|
|
if (!ppro.FrameRate || typeof ppro.FrameRate.createWithValue !== "function" ||
|
|
!ppro.TickTime || typeof ppro.TickTime.createWithSeconds !== "function" ||
|
|
typeof ppro.TickTime.createWithFrameAndFrameRate !== "function") {
|
|
throw commandError("UXP_COMMAND_UNAVAILABLE", "This Premiere build cannot construct native frame-aligned TickTime values");
|
|
}
|
|
let frameRate;
|
|
try { frameRate = ppro.FrameRate.createWithValue(requestedFrameRate); } catch (_) {
|
|
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere rejected the requested frame rate");
|
|
}
|
|
const frameRateSnapshot = nativeFrameRateSnapshot(frameRate, requestedFrameRate);
|
|
if (action === "align") {
|
|
if (args.frameCount !== undefined || args.seconds === undefined) {
|
|
throw commandError("UXP_INVALID_ARGUMENT", "align requires seconds and does not accept frameCount");
|
|
}
|
|
const seconds = boundedFiniteNumber(args.seconds, "seconds", 0, MAX_TIME_ALIGNMENT_SECONDS);
|
|
let value, alignedDown, alignedNearest;
|
|
try {
|
|
value = ppro.TickTime.createWithSeconds(seconds);
|
|
if (!value || typeof value.alignToFrame !== "function" || typeof value.alignToNearestFrame !== "function") {
|
|
throw new Error("missing frame-alignment methods");
|
|
}
|
|
alignedDown = value.alignToFrame(frameRate);
|
|
alignedNearest = value.alignToNearestFrame(frameRate);
|
|
} catch (_) {
|
|
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere could not align the requested TickTime to the supplied frame rate");
|
|
}
|
|
return {
|
|
action,
|
|
requested: { seconds, frameRate: requestedFrameRate },
|
|
frameRate: frameRateSnapshot,
|
|
input: nativeTickTimeSnapshot(value, "input TickTime"),
|
|
aligned: {
|
|
down: nativeTickTimeSnapshot(alignedDown, "frame-aligned TickTime"),
|
|
nearest: nativeTickTimeSnapshot(alignedNearest, "nearest-frame TickTime")
|
|
},
|
|
verificationBoundary: "native_ticktime_value_readback"
|
|
};
|
|
}
|
|
if (args.seconds !== undefined || args.frameCount === undefined) {
|
|
throw commandError("UXP_INVALID_ARGUMENT", "frame requires frameCount and does not accept seconds");
|
|
}
|
|
const frameCount = boundedInteger(args.frameCount, "frameCount", 0, MAX_TIME_ALIGNMENT_FRAME_COUNT);
|
|
let value;
|
|
try { value = ppro.TickTime.createWithFrameAndFrameRate(frameCount, frameRate); } catch (_) {
|
|
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere could not construct the requested frame TickTime");
|
|
}
|
|
return {
|
|
action,
|
|
requested: { frameCount, frameRate: requestedFrameRate },
|
|
frameRate: frameRateSnapshot,
|
|
value: nativeTickTimeSnapshot(value, "frame TickTime"),
|
|
verificationBoundary: "native_ticktime_value_readback"
|
|
};
|
|
}
|
|
async function inspectSequenceTiming(args) {
|
|
assertOnlyKeys(args, []);
|
|
const context = await activeContext(false);
|
|
const snapshot = await sequenceTimingSnapshot(context.sequence);
|
|
// Every timing field is a native asynchronous read. Re-resolve the active
|
|
// sequence after that read set so the returned snapshot belongs to the
|
|
// same active sequence at both the start and end of this request. The
|
|
// documented API does not provide an atomic snapshot or an activation
|
|
// revision, so a transient switch back to the same sequence is not
|
|
// distinguishable from an unchanged active sequence.
|
|
const current = await activeContext(false);
|
|
if (sequenceGuidRequired(current.sequence) !== snapshot.sequenceGuid) {
|
|
throw commandError("UXP_STALE_SEQUENCE", "The active sequence no longer matches the timing snapshot; retry the inspection");
|
|
}
|
|
return { ...snapshot, verificationBoundary: "sequence_timing_readback" };
|
|
}
|
|
async function inspectSequenceTimingByGuid(args) {
|
|
assertOnlyKeys(args, ["sequenceGuid"]);
|
|
const requestedSequenceGuid = sequenceGuidArgument(args.sequenceGuid, "sequenceGuid");
|
|
const project = await ppro.Project.getActiveProject();
|
|
if (!project) throw commandError("UXP_NO_ACTIVE_PROJECT", "No active project");
|
|
if (typeof project.getSequence !== "function") {
|
|
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere cannot resolve a sequence by GUID");
|
|
}
|
|
const projectGuid = requiredProjectGuid(project);
|
|
const firstSequence = await project.getSequence(requestedSequenceGuid.native);
|
|
if (!firstSequence) throw commandError("UXP_TARGET_NOT_FOUND", "sequenceGuid was not found in the active project");
|
|
const first = await sequenceTimingSnapshot(firstSequence);
|
|
if (first.sequenceGuid !== requestedSequenceGuid.text) {
|
|
throw commandError("UXP_STALE_SEQUENCE", "The requested sequence GUID did not match Premiere's resolved sequence");
|
|
}
|
|
// Re-resolve the active project and the requested sequence after every
|
|
// asynchronous timing read. Project.getSequence() is the documented
|
|
// targeted lookup; it avoids activating a different timeline just to
|
|
// inspect it. Premiere exposes no atomic sequence-timing snapshot, so
|
|
// require a complete second equal read rather than returning a mixed
|
|
// first snapshot when the target changes during inspection.
|
|
const currentProject = await ppro.Project.getActiveProject();
|
|
if (!currentProject || requiredProjectGuid(currentProject) !== projectGuid ||
|
|
typeof currentProject.getSequence !== "function") {
|
|
throw commandError("UXP_STALE_PROJECT", "The active project changed while reading the target sequence; retry the inspection");
|
|
}
|
|
const finalSequence = await currentProject.getSequence(requestedSequenceGuid.native);
|
|
if (!finalSequence) throw commandError("UXP_STALE_SEQUENCE", "The requested sequence changed or was removed while reading it; retry the inspection");
|
|
const final = await sequenceTimingSnapshot(finalSequence);
|
|
if (final.sequenceGuid !== requestedSequenceGuid.text || !sameSequenceTimingSnapshot(first, final)) {
|
|
throw commandError("UXP_STALE_SEQUENCE", "The requested sequence timing changed while reading it; retry the inspection");
|
|
}
|
|
return {
|
|
...final,
|
|
projectGuid,
|
|
requestedSequenceGuid: requestedSequenceGuid.text,
|
|
verificationBoundary: "targeted_sequence_timing_double_readback"
|
|
};
|
|
}
|
|
async function setSequencePlayhead(args) {
|
|
const input = validateSequencePlayheadSetArgs(args);
|
|
// TickTime construction can cross the host boundary. Construct it before
|
|
// the per-sequence exclusion scope so the guarded snapshot is the final
|
|
// asynchronous preflight step before the host setter is invoked.
|
|
const position = await tickTime(input.positionSeconds, "positionSeconds");
|
|
return withSequencePlayheadSetLock(input.expectedSequenceGuid, async () => {
|
|
const context = await activeContext(false);
|
|
const before = await sequencePlayheadSnapshot(context.sequence);
|
|
if (before.sequenceGuid !== input.expectedSequenceGuid) {
|
|
throw commandError("UXP_STALE_SEQUENCE", "The active sequence changed before the player position was set; inspect the current playhead and retry");
|
|
}
|
|
if (!sameSeconds(before.positionSeconds, input.expectedPositionSeconds)) {
|
|
throw commandError("UXP_STALE_PLAYHEAD", "The sequence player position changed before it was set; inspect the current playhead and retry");
|
|
}
|
|
const accepted = await context.sequence.setPlayerPosition(position);
|
|
if (accepted !== true) throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not confirm the sequence player position");
|
|
// Re-resolve the active sequence for postcondition readback: a user can
|
|
// switch sequences while Premiere awaits the host setter.
|
|
const after = await sequencePlayheadSnapshot((await activeContext(false)).sequence);
|
|
if (after.sequenceGuid !== before.sequenceGuid || !sameSeconds(after.positionSeconds, input.positionSeconds)) {
|
|
throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not retain the requested sequence player position");
|
|
}
|
|
return {
|
|
positioned: true,
|
|
outcome: "verified",
|
|
sequenceGuid: after.sequenceGuid,
|
|
positionSeconds: after.positionSeconds,
|
|
verified: "sequence_playhead_readback",
|
|
operation: operationSemantics({
|
|
mutatesProject: false,
|
|
verificationStatus: "verified",
|
|
verificationBoundary: "sequence_playhead_readback",
|
|
verificationEvidence: [{ type: "sequence_playhead", sequenceGuid: after.sequenceGuid, positionSeconds: after.positionSeconds }],
|
|
undoSupported: false,
|
|
cancellationSupported: false
|
|
})
|
|
};
|
|
});
|
|
}
|
|
function withSequencePlayheadSetLock(sequenceGuid, operation) {
|
|
const previous = sequencePlayheadSetTails.get(sequenceGuid) || Promise.resolve();
|
|
let release;
|
|
const gate = new Promise((resolve) => { release = resolve; });
|
|
const tail = previous.catch(() => undefined).then(() => gate);
|
|
sequencePlayheadSetTails.set(sequenceGuid, tail);
|
|
return previous.catch(() => undefined).then(operation).finally(() => {
|
|
release();
|
|
if (sequencePlayheadSetTails.get(sequenceGuid) === tail) sequencePlayheadSetTails.delete(sequenceGuid);
|
|
});
|
|
}
|
|
function inspectAppPreferences(args) {
|
|
assertOnlyKeys(args, []);
|
|
return {
|
|
preferences: Object.keys(APP_PREFERENCE_KEYS).map((preference) => appPreferenceSnapshot(preference)),
|
|
verificationBoundary: "app_preference_native_string_readback"
|
|
};
|
|
}
|
|
function setAppPreference(args) {
|
|
const input = validateAppPreferenceSetArgs(args);
|
|
return withAppPreferenceSetLock(input.preference, () => {
|
|
const before = appPreferenceSnapshot(input.preference);
|
|
if (before.value !== input.expectedValue) {
|
|
throw commandError("UXP_STALE_APP_PREFERENCE", "The app preference changed before it was set; inspect current preferences and retry");
|
|
}
|
|
const persistenceFlag = ppro.AppPreference[input.persistence === "persistent" ? "PROPERTY_PERSISTENT" : "PROPERTY_NON_PERSISTENT"];
|
|
const accepted = ppro.AppPreference.setValue(preferenceNativeKey(input.preference), input.value, persistenceFlag);
|
|
if (accepted !== true) throw commandError("UXP_ACTION_REJECTED", "Premiere rejected the app preference update");
|
|
const after = appPreferenceSnapshot(input.preference);
|
|
if (after.value !== input.value) {
|
|
throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not retain the requested app preference value");
|
|
}
|
|
return {
|
|
updated: true,
|
|
outcome: "verified",
|
|
preference: input.preference,
|
|
beforeValue: before.value,
|
|
value: after.value,
|
|
persistence: input.persistence,
|
|
verified: "app_preference_native_string_readback",
|
|
operation: operationSemantics({
|
|
// Adobe exposes AppPreference as application state, not a project
|
|
// action or transaction. Do not claim project mutation or Undo.
|
|
mutatesProject: false,
|
|
verificationStatus: "verified",
|
|
verificationBoundary: "app_preference_native_string_readback",
|
|
verificationEvidence: [{ type: "app_preference", preference: input.preference, beforeValue: before.value, value: after.value, persistence: input.persistence }],
|
|
undoSupported: false,
|
|
cancellationSupported: false
|
|
})
|
|
};
|
|
});
|
|
}
|
|
function appPreferenceSnapshot(preference) {
|
|
const value = ppro.AppPreference.getValue(preferenceNativeKey(preference));
|
|
if (typeof value !== "string" || value.length > MAX_APP_PREFERENCE_VALUE_CHARS) {
|
|
throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not return a bounded native string app preference value");
|
|
}
|
|
return { preference, value };
|
|
}
|
|
function preferenceNativeKey(preference) {
|
|
const property = APP_PREFERENCE_KEYS[preference], value = property && ppro.AppPreference && ppro.AppPreference[property];
|
|
if (typeof value !== "string" || !value) throw commandError("UXP_COMMAND_UNAVAILABLE", "This Premiere build does not expose the requested app preference key");
|
|
return value;
|
|
}
|
|
function withAppPreferenceSetLock(preference, operation) {
|
|
const previous = appPreferenceSetTails.get(preference) || Promise.resolve();
|
|
let release;
|
|
const gate = new Promise((resolve) => { release = resolve; });
|
|
const tail = previous.catch(() => undefined).then(() => gate);
|
|
appPreferenceSetTails.set(preference, tail);
|
|
return previous.catch(() => undefined).then(operation).finally(() => {
|
|
release();
|
|
if (appPreferenceSetTails.get(preference) === tail) appPreferenceSetTails.delete(preference);
|
|
});
|
|
}
|
|
async function exportInterchange(args) {
|
|
assertOnlyKeys(args, ["format", "outputFilePath", "suppressUI", "operationId"]);
|
|
const format = requiredString(args.format, "format"), outputFilePath = await allowedPath(args.outputFilePath, "outputFilePath", "file");
|
|
if (format !== "otio" && format !== "fcpxml") throw commandError("UXP_INVALID_ARGUMENT", "format must be otio or fcpxml");
|
|
const context = await activeContext(false);
|
|
const method = format === "otio" ? "exportAsOpenTimelineIO" : "exportAsFinalCutProXML";
|
|
const exported = await ppro.ProjectConverter[method](context.sequence, outputFilePath, args.suppressUI !== false);
|
|
if (!exported) throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not confirm the interchange export");
|
|
return { exported: true, outcome: "verified", format, outputFilePath };
|
|
}
|
|
async function exportAaf(args) {
|
|
const input = validateAafArgs(args);
|
|
input.outputFilePath = await allowedPath(input.outputFilePath, "outputFilePath", "file");
|
|
if (input.options.videoMixdownPresetPath != null) {
|
|
input.options.videoMixdownPresetPath = await allowedPath(input.options.videoMixdownPresetPath, "videoMixdownPresetPath", "file");
|
|
}
|
|
const context = await activeContext(false);
|
|
const options = buildAafExportOptions(ppro, input.options);
|
|
const exported = await ppro.ProjectConverter.exportAAF(context.sequence, input.outputFilePath, options);
|
|
if (!exported) throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not confirm the AAF export");
|
|
// ProjectConverter confirms the export request, but UXP cannot reliably stat arbitrary
|
|
// native paths after Premiere returns. Keep that evidence boundary explicit.
|
|
return {
|
|
exported: true, outcome: "committed_unverified", format: "aaf", outputFilePath: input.outputFilePath,
|
|
options: input.options, outputVerified: false, verificationBoundary: "projectConverter_exportAAF_return",
|
|
operation: operationSemantics({
|
|
mutatesProject: false, verificationStatus: "not_verified", verificationBoundary: "projectConverter_exportAAF_return",
|
|
verificationEvidence: [{ type: "host_return", value: true }], cancellationSupported: true
|
|
})
|
|
};
|
|
}
|
|
async function renameTrack(args) {
|
|
const input = validateRenameTrackArgs(args), context = await activeContext(true);
|
|
const track = await trackAt(context.sequence, input.trackType, input.trackIndex);
|
|
if (typeof track.createSetNameAction !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "This " + input.trackType + " track cannot be renamed by this Premiere build");
|
|
let committed = false;
|
|
context.project.lockedAccess(() => {
|
|
const action = track.createSetNameAction(input.name);
|
|
committed = context.project.executeTransaction((compoundAction) => {
|
|
if (!action || compoundAction.addAction(action) === false) throw commandError("UXP_ACTION_REJECTED", "Premiere rejected the track rename action");
|
|
}, "Rename " + trackLabel(input.trackType));
|
|
});
|
|
assertTransactionCommitted(committed, "track rename");
|
|
const verifiedTrack = await trackAt(context.sequence, input.trackType, input.trackIndex);
|
|
if (String(verifiedTrack.name || "") !== input.name) throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not retain the requested track name");
|
|
return {
|
|
renamed: true, outcome: "verified", trackType: input.trackType, trackIndex: input.trackIndex,
|
|
name: String(verifiedTrack.name || ""), verified: "track_name_readback",
|
|
operation: operationSemantics({
|
|
mutatesProject: true, verificationStatus: "verified", verificationBoundary: "track_name_readback",
|
|
verificationEvidence: [{ type: "track", trackType: input.trackType, trackIndex: input.trackIndex, name: input.name }],
|
|
undoSupported: true, undoLabel: "Rename " + trackLabel(input.trackType), transactionActionGroup: true, cancellationSupported: true
|
|
})
|
|
};
|
|
}
|
|
async function createSubclip(args) {
|
|
const input = validateSubclipArgs(args);
|
|
const context = await activeContext(true);
|
|
const clip = await resolveClipProjectItem(context.project, input);
|
|
if (typeof clip.createSubClipAction !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "This Premiere build cannot create subclips through UXP");
|
|
const before = await projectItemInventory(context.project);
|
|
const startTime = await tickTime(input.startSeconds, "startSeconds"), endTime = await tickTime(input.endSeconds, "endSeconds");
|
|
let committed = false;
|
|
context.project.lockedAccess(() => {
|
|
const action = clip.createSubClipAction(
|
|
input.name, startTime, endTime, input.hasHardBoundaries,
|
|
{ takeVideo: input.takeVideo, takeAudio: input.takeAudio }
|
|
);
|
|
committed = context.project.executeTransaction((compoundAction) => {
|
|
if (!action || compoundAction.addAction(action) === false) throw commandError("UXP_ACTION_REJECTED", "Premiere rejected the subclip creation action");
|
|
}, "Create subclip");
|
|
});
|
|
assertTransactionCommitted(committed, "subclip creation");
|
|
const created = await findCreatedSubclip(context.project, before, input.name);
|
|
if (!created) throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not expose the created subclip in the project inventory");
|
|
return {
|
|
created: true, outcome: "verified", subclip: created,
|
|
sourceProjectItemId: await projectItemIdentifier(clip), sourceProjectItemName: String(clip.name || ""),
|
|
startSeconds: input.startSeconds, endSeconds: input.endSeconds,
|
|
hasHardBoundaries: input.hasHardBoundaries, takeVideo: input.takeVideo, takeAudio: input.takeAudio,
|
|
verified: "new_project_item_readback",
|
|
operation: operationSemantics({
|
|
mutatesProject: true, verificationStatus: "verified", verificationBoundary: "new_project_item_readback",
|
|
verificationEvidence: [{ type: "project_item", id: created.projectItemId, name: created.name }],
|
|
undoSupported: true, undoLabel: "Create subclip", transactionActionGroup: true, cancellationSupported: true
|
|
})
|
|
};
|
|
}
|
|
async function listMarkers(args) {
|
|
const input = validateMarkerListArgs(args);
|
|
const project = await ppro.Project.getActiveProject();
|
|
if (!project) throw commandError("UXP_NO_ACTIVE_PROJECT", "No active project");
|
|
const owner = input.scope === "sequence"
|
|
? await activeSequence(project)
|
|
: await resolveClipProjectItem(project, input);
|
|
if (!ppro.Markers || typeof ppro.Markers.getMarkers !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "This Premiere build does not expose documented marker APIs");
|
|
const collection = await ppro.Markers.getMarkers(owner);
|
|
if (!collection || typeof collection.getMarkers !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere did not return a marker collection");
|
|
const source = Array.from(await collection.getMarkers(input.filters) || []);
|
|
const markers = [];
|
|
for (let i = 0; i < source.length; i += 1) markers.push(await markerSnapshot(source[i], input.includeWebLinks, input.includeColorValues));
|
|
return {
|
|
scope: input.scope, ownerGuid: input.scope === "sequence" ? stringifyGuid(owner.guid) : null,
|
|
ownerProjectItemId: input.scope === "projectItem" ? await projectItemIdentifier(owner) : null,
|
|
filters: input.filters, count: markers.length, markers
|
|
};
|
|
}
|
|
async function setSourceMonitorPosition(args) {
|
|
assertOnlyKeys(args, ["seconds", "operationId"]);
|
|
const seconds = nonNegativeNumber(args.seconds, "seconds");
|
|
const position = await tickTime(seconds, "seconds");
|
|
const sourceMonitor = ppro.SourceMonitor;
|
|
if (!sourceMonitor || typeof sourceMonitor.setPosition !== "function" || typeof sourceMonitor.getPosition !== "function") {
|
|
throw commandError("UXP_COMMAND_UNAVAILABLE", "This Premiere build does not expose Source Monitor positioning");
|
|
}
|
|
const set = await sourceMonitor.setPosition(position);
|
|
if (!set) throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not confirm the Source Monitor position update");
|
|
const readback = await sourceMonitor.getPosition();
|
|
const readbackSeconds = tickSeconds(readback);
|
|
if (readbackSeconds == null || Math.abs(readbackSeconds - seconds) > 0.000001) {
|
|
throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not retain the requested Source Monitor position");
|
|
}
|
|
return {
|
|
positioned: true, outcome: "verified", seconds: readbackSeconds, verified: "source_monitor_position_readback",
|
|
operation: operationSemantics({
|
|
mutatesProject: false, verificationStatus: "verified", verificationBoundary: "source_monitor_position_readback",
|
|
verificationEvidence: [{ type: "source_monitor_position", seconds: readbackSeconds }], cancellationSupported: true
|
|
})
|
|
};
|
|
}
|
|
async function transcriptLanguages() {
|
|
const languages = Array.from(await ppro.Transcript.querySupportedLanguages() || []);
|
|
return { languages, count: languages.length };
|
|
}
|
|
async function hasObjectMask(args) {
|
|
assertOnlyKeys(args, ["scope"]);
|
|
const scope = args.scope == null ? "sequence" : args.scope;
|
|
if (scope !== "sequence" && scope !== "project") throw commandError("UXP_INVALID_ARGUMENT", "scope must be sequence or project");
|
|
const project = await ppro.Project.getActiveProject();
|
|
if (!project) throw commandError("UXP_NO_ACTIVE_PROJECT", "No active project");
|
|
const target = scope === "project" ? project : await project.getActiveSequence();
|
|
if (!target) throw commandError("UXP_NO_ACTIVE_SEQUENCE", "No active sequence");
|
|
return { scope, hasObjectMask: !!ppro.ObjectMaskUtils.hasObjectMask(target) };
|
|
}
|
|
async function configureEncoder(args) {
|
|
assertOnlyKeys(args, ["launch", "embeddedXmp", "sidecarXmp", "startBatch", "operationId"]);
|
|
const performed = [];
|
|
if (args.launch) { if (!await ppro.EncoderManager.launchEncoder()) throw commandError("UXP_VERIFICATION_FAILED", "AME did not launch"); performed.push("launch"); }
|
|
if (args.embeddedXmp != null) { await ppro.EncoderManager.setEmbeddedXMPEnabled(!!args.embeddedXmp); performed.push("embeddedXmp"); }
|
|
if (args.sidecarXmp != null) { await ppro.EncoderManager.setSidecarXMPEnabled(!!args.sidecarXmp); performed.push("sidecarXmp"); }
|
|
if (args.startBatch) { if (!await ppro.EncoderManager.startBatchEncode()) throw commandError("UXP_VERIFICATION_FAILED", "AME batch queue did not start"); performed.push("startBatch"); }
|
|
if (!performed.length) throw commandError("UXP_INVALID_ARGUMENT", "At least one encoder operation is required");
|
|
return { configured: true, outcome: "committed_unverified", performed };
|
|
}
|
|
async function tickTime(seconds, name) {
|
|
const value = Number(seconds);
|
|
if (!Number.isFinite(value) || value < 0) throw commandError("UXP_INVALID_ARGUMENT", name + " must be a non-negative number");
|
|
if (ppro.TickTime && typeof ppro.TickTime.createWithSeconds === "function") return ppro.TickTime.createWithSeconds(value);
|
|
throw commandError("UXP_COMMAND_UNAVAILABLE", "This Premiere build cannot create TickTime");
|
|
}
|
|
async function sequenceRangeSnapshot(sequence) {
|
|
const sequenceGuid = String(sequence && sequence.guid || "");
|
|
if (!sequenceGuid) throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not provide a stable active-sequence GUID");
|
|
const [inPoint, outPoint, zeroPoint, endPoint] = await Promise.all([
|
|
sequence.getInPoint(), sequence.getOutPoint(), sequence.getZeroPoint(), sequence.getEndTime()
|
|
]);
|
|
const range = {
|
|
inSeconds: tickSecondsRequired(inPoint, "sequence in point"),
|
|
outSeconds: tickSecondsRequired(outPoint, "sequence out point"),
|
|
zeroPointSeconds: tickSecondsRequired(zeroPoint, "sequence zero point"),
|
|
endSeconds: tickSecondsRequired(endPoint, "sequence end point")
|
|
};
|
|
assertValidSequenceRange(range, "Premiere sequence range");
|
|
return { sequenceGuid, range };
|
|
}
|
|
async function sequencePlayheadSnapshot(sequence) {
|
|
const sequenceGuid = sequenceGuidRequired(sequence);
|
|
return {
|
|
sequenceGuid,
|
|
positionSeconds: tickSecondsRequired(await sequence.getPlayerPosition(), "sequence player position")
|
|
};
|
|
}
|
|
async function sequenceTimingSnapshot(sequence) {
|
|
const sequenceGuid = sequenceGuidRequired(sequence);
|
|
const [frameSize, timebase, audioDisplay, videoDisplay, projectItem] = await Promise.all([
|
|
sequence.getFrameSize(),
|
|
sequence.getTimebase(),
|
|
sequence.getSequenceAudioTimeDisplayFormat(),
|
|
sequence.getSequenceVideoTimeDisplayFormat(),
|
|
sequence.getProjectItem()
|
|
]);
|
|
if (!projectItem || typeof projectItem.getId !== "function") {
|
|
throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not return an identifiable sequence project item");
|
|
}
|
|
const projectItemId = await projectItem.getId();
|
|
if (typeof projectItemId !== "string" || !projectItemId.trim() || projectItemId.length > 512) {
|
|
throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not return a valid sequence project-item ID");
|
|
}
|
|
return {
|
|
sequenceGuid,
|
|
sequenceName: snapshotString(sequence.name, "sequence name", 255),
|
|
frameSize: frameSizeSnapshot(frameSize),
|
|
timebase: timebaseSnapshot(timebase),
|
|
audioTimeDisplayFormat: timeDisplaySnapshot(audioDisplay, "sequence audio time display format"),
|
|
videoTimeDisplayFormat: timeDisplaySnapshot(videoDisplay, "sequence video time display format"),
|
|
projectItem: {
|
|
id: projectItemId,
|
|
name: snapshotString(projectItem.name, "sequence project-item name", 255)
|
|
}
|
|
};
|
|
}
|
|
function sequenceGuidRequired(sequence) {
|
|
const rawGuid = sequence && sequence.guid;
|
|
const isStringGuid = typeof rawGuid === "string";
|
|
const isGuidObject = rawGuid && typeof rawGuid === "object" &&
|
|
typeof rawGuid.toString === "function" && rawGuid.toString !== Object.prototype.toString;
|
|
if (!isStringGuid && !isGuidObject) {
|
|
throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not provide a stable active-sequence GUID");
|
|
}
|
|
let sequenceGuid;
|
|
try { sequenceGuid = isStringGuid ? rawGuid : rawGuid.toString(); } catch (_) { sequenceGuid = ""; }
|
|
if (typeof sequenceGuid !== "string" || !sequenceGuid.trim() || sequenceGuid !== sequenceGuid.trim() || sequenceGuid.length > 512) {
|
|
throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not provide a stable active-sequence GUID");
|
|
}
|
|
return sequenceGuid;
|
|
}
|
|
function sequenceGuidArgument(value, name) {
|
|
if (typeof value !== "string" || !value.trim() || value !== value.trim() || value.length > 512) {
|
|
throw commandError("UXP_INVALID_ARGUMENT", name + " is required and must be a bounded GUID string");
|
|
}
|
|
if (!ppro.Guid || typeof ppro.Guid.fromString !== "function") {
|
|
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere cannot parse sequence GUIDs");
|
|
}
|
|
let native;
|
|
try { native = ppro.Guid.fromString(value); } catch (_) {
|
|
throw commandError("UXP_INVALID_ARGUMENT", name + " is not a valid Premiere GUID");
|
|
}
|
|
if (!native) throw commandError("UXP_INVALID_ARGUMENT", name + " is not a valid Premiere GUID");
|
|
return { text: value, native };
|
|
}
|
|
function sameSequenceTimingSnapshot(left, right) {
|
|
return left && right &&
|
|
left.sequenceGuid === right.sequenceGuid &&
|
|
left.sequenceName === right.sequenceName &&
|
|
left.timebase === right.timebase &&
|
|
left.frameSize && right.frameSize &&
|
|
left.frameSize.width === right.frameSize.width && left.frameSize.height === right.frameSize.height &&
|
|
left.audioTimeDisplayFormat && right.audioTimeDisplayFormat &&
|
|
left.audioTimeDisplayFormat.type === right.audioTimeDisplayFormat.type &&
|
|
left.videoTimeDisplayFormat && right.videoTimeDisplayFormat &&
|
|
left.videoTimeDisplayFormat.type === right.videoTimeDisplayFormat.type &&
|
|
left.projectItem && right.projectItem &&
|
|
left.projectItem.id === right.projectItem.id && left.projectItem.name === right.projectItem.name;
|
|
}
|
|
function frameSizeSnapshot(value) {
|
|
if (!value || typeof value.width !== "number" || typeof value.height !== "number" ||
|
|
!Number.isSafeInteger(value.width) || !Number.isSafeInteger(value.height) ||
|
|
value.width < 1 || value.height < 1 ||
|
|
value.width > MAX_SEQUENCE_FRAME_WIDTH || value.height > MAX_SEQUENCE_FRAME_HEIGHT) {
|
|
throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not return a valid sequence frame size");
|
|
}
|
|
return { width: value.width, height: value.height };
|
|
}
|
|
function timebaseSnapshot(value) {
|
|
if (typeof value !== "string" || !new RegExp("^[1-9]\\d{0," + (MAX_SEQUENCE_TIMEBASE_DIGITS - 1) + "}$").test(value)) {
|
|
throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not return a valid sequence timebase");
|
|
}
|
|
return value;
|
|
}
|
|
function timeDisplaySnapshot(value, name) {
|
|
if (!value || typeof value.type !== "number" || !Number.isSafeInteger(value.type) || value.type < 0) {
|
|
throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not return a valid " + name);
|
|
}
|
|
return { type: value.type };
|
|
}
|
|
function nativeFrameRateSnapshot(value, requested) {
|
|
if (!value || !Number.isFinite(Number(value.value)) || Number(value.value) < 1 || Number(value.value) > 240 ||
|
|
Math.abs(Number(value.value) - requested) > 0.000001 || !Number.isFinite(Number(value.ticksPerFrame)) || Number(value.ticksPerFrame) <= 0) {
|
|
throw commandError("UXP_INVALID_HOST_STATE", "Premiere did not return the requested valid native frame rate");
|
|
}
|
|
return { value: Number(value.value), ticksPerFrame: Number(value.ticksPerFrame) };
|
|
}
|
|
function nativeTickTimeSnapshot(value, name) {
|
|
if (!value || !Number.isFinite(Number(value.seconds)) || Number(value.seconds) < 0 || Number(value.seconds) > MAX_TIME_ALIGNMENT_SECONDS ||
|
|
typeof value.ticks !== "string" || !/^\d{1,18}$/.test(value.ticks)) {
|
|
throw commandError("UXP_INVALID_HOST_STATE", "Premiere did not return a valid " + name);
|
|
}
|
|
return { seconds: Number(value.seconds), ticks: value.ticks };
|
|
}
|
|
function boundedFiniteNumber(value, name, minimum, maximum) {
|
|
const number = Number(value);
|
|
if (!Number.isFinite(number) || number < minimum || number > maximum) {
|
|
throw commandError("UXP_INVALID_ARGUMENT", name + " must be a finite number from " + minimum + " through " + maximum);
|
|
}
|
|
return number;
|
|
}
|
|
function boundedInteger(value, name, minimum, maximum) {
|
|
if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
|
|
throw commandError("UXP_INVALID_ARGUMENT", name + " must be an integer from " + minimum + " through " + maximum);
|
|
}
|
|
return value;
|
|
}
|
|
function snapshotString(value, name, maximum) {
|
|
if (typeof value !== "string" || !value.trim() || value.length > maximum) {
|
|
throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not return a valid " + name);
|
|
}
|
|
return value;
|
|
}
|
|
function validateSequenceRangeUpdateArgs(args) {
|
|
assertObject(args);
|
|
assertOnlyKeys(args, ["expectedSequenceGuid", "expectedRange", "updates", "operationId"]);
|
|
if (typeof args.expectedSequenceGuid !== "string" || !args.expectedSequenceGuid || args.expectedSequenceGuid.length > 512) {
|
|
throw commandError("UXP_INVALID_ARGUMENT", "expectedSequenceGuid is required and must be at most 512 characters");
|
|
}
|
|
const expectedRange = validateExpectedSequenceRange(args.expectedRange, "expectedRange");
|
|
const updates = validateSequenceRangeUpdates(args.updates);
|
|
if (updates.inSeconds == null && updates.outSeconds == null && updates.zeroPointSeconds == null) {
|
|
throw commandError("UXP_INVALID_ARGUMENT", "updates must include at least one sequence range field");
|
|
}
|
|
return { expectedSequenceGuid: args.expectedSequenceGuid, expectedRange, updates };
|
|
}
|
|
function validateSequencePlayheadSetArgs(args) {
|
|
assertObject(args);
|
|
assertOnlyKeys(args, ["expectedSequenceGuid", "expectedPositionSeconds", "positionSeconds", "operationId"]);
|
|
if (typeof args.expectedSequenceGuid !== "string" || !args.expectedSequenceGuid || args.expectedSequenceGuid.length > 512) {
|
|
throw commandError("UXP_INVALID_ARGUMENT", "expectedSequenceGuid is required and must be at most 512 characters");
|
|
}
|
|
return {
|
|
expectedSequenceGuid: args.expectedSequenceGuid,
|
|
expectedPositionSeconds: boundedSeconds(args.expectedPositionSeconds, "expectedPositionSeconds"),
|
|
positionSeconds: boundedSeconds(args.positionSeconds, "positionSeconds")
|
|
};
|
|
}
|
|
function validateExpectedSequenceRange(value, name) {
|
|
assertObject(value);
|
|
assertOnlyKeys(value, ["inSeconds", "outSeconds", "zeroPointSeconds", "endSeconds"]);
|
|
const result = {};
|
|
for (const key of ["inSeconds", "outSeconds", "zeroPointSeconds", "endSeconds"]) {
|
|
if (value[key] == null) {
|
|
throw commandError("UXP_INVALID_ARGUMENT", name + "." + key + " is required");
|
|
}
|
|
result[key] = boundedSeconds(value[key], name + "." + key);
|
|
}
|
|
return result;
|
|
}
|
|
function validateSequenceRangeUpdates(value) {
|
|
assertObject(value);
|
|
assertOnlyKeys(value, ["inSeconds", "outSeconds", "zeroPointSeconds"]);
|
|
const result = {};
|
|
for (const key of ["inSeconds", "outSeconds", "zeroPointSeconds"]) {
|
|
if (value[key] != null) result[key] = boundedSeconds(value[key], "updates." + key);
|
|
}
|
|
return result;
|
|
}
|
|
function assertExpectedSequenceRange(actual, expected) {
|
|
if (!sameSeconds(actual.inSeconds, expected.inSeconds) || !sameSeconds(actual.outSeconds, expected.outSeconds) ||
|
|
!sameSeconds(actual.zeroPointSeconds, expected.zeroPointSeconds) || !sameSeconds(actual.endSeconds, expected.endSeconds)) {
|
|
throw commandError("UXP_STALE_RANGE", "The sequence range changed before the update; inspect the current range and retry");
|
|
}
|
|
}
|
|
function assertValidSequenceRange(range, name) {
|
|
const inSeconds = boundedSeconds(range.inSeconds, name + ".inSeconds");
|
|
const outSeconds = boundedSeconds(range.outSeconds, name + ".outSeconds");
|
|
const endSeconds = boundedSeconds(range.endSeconds, name + ".endSeconds");
|
|
boundedSeconds(range.zeroPointSeconds, name + ".zeroPointSeconds");
|
|
if (inSeconds > outSeconds || outSeconds > endSeconds) {
|
|
throw commandError("UXP_INVALID_ARGUMENT", name + " must satisfy inSeconds <= outSeconds <= endSeconds");
|
|
}
|
|
}
|
|
function sameSequenceRange(actual, expected) {
|
|
return sameSeconds(actual.inSeconds, expected.inSeconds) && sameSeconds(actual.outSeconds, expected.outSeconds) &&
|
|
sameSeconds(actual.zeroPointSeconds, expected.zeroPointSeconds) && sameSeconds(actual.endSeconds, expected.endSeconds);
|
|
}
|
|
function sameSeconds(left, right) {
|
|
return typeof left === "number" && typeof right === "number" && Math.abs(left - right) <= 0.000001;
|
|
}
|
|
function tickSecondsRequired(value, name) {
|
|
if (!value || typeof value.seconds !== "number" || !Number.isFinite(value.seconds) || value.seconds < 0) {
|
|
throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not return a valid " + name);
|
|
}
|
|
return value.seconds;
|
|
}
|
|
function boundedSeconds(value, name) {
|
|
if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 86400) {
|
|
throw commandError("UXP_INVALID_ARGUMENT", name + " must be a finite number from 0 through 86400");
|
|
}
|
|
return value;
|
|
}
|
|
async function activeContext(requireMutationApis) {
|
|
const project = await ppro.Project.getActiveProject();
|
|
if (!project) throw commandError("UXP_NO_ACTIVE_PROJECT", "No active project");
|
|
const sequence = await project.getActiveSequence();
|
|
if (!sequence) throw commandError("UXP_NO_ACTIVE_SEQUENCE", "No active sequence");
|
|
if (requireMutationApis && (typeof project.lockedAccess !== "function" || typeof project.executeTransaction !== "function")) {
|
|
throw commandError("UXP_COMMAND_UNAVAILABLE", "This Premiere build does not expose locked undoable transactions");
|
|
}
|
|
return { project, sequence };
|
|
}
|
|
async function exportFrame(args) {
|
|
const context = await activeContext(false);
|
|
if (!args.outputDirectory) throw commandError("UXP_INVALID_ARGUMENT", "outputDirectory is required");
|
|
const outputDirectory = await allowedPath(args.outputDirectory, "outputDirectory", "directory");
|
|
const filename = Protocol.safeFilename(args.filename);
|
|
const exporterFilename = Protocol.exporterFrameName(filename);
|
|
const position = args.seconds == null ? await context.sequence.getPlayerPosition() : await tickTime(args.seconds, "seconds");
|
|
const size = await context.sequence.getFrameSize();
|
|
const width = positiveInt(args.width, size.width, "width"), height = positiveInt(args.height, size.height, "height");
|
|
const returned = await ppro.Exporter.exportSequenceFrame(context.sequence, position, exporterFilename, outputDirectory, width, height);
|
|
if (returned !== true) throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not confirm frame export; no output path is reported");
|
|
const path = Protocol.joinPath(outputDirectory, filename);
|
|
return { path, width, height, seconds: position.seconds, exporterResult: returned };
|
|
}
|
|
async function liftSelection(args) {
|
|
const input = validateLiftArgs(args), context = await activeContext(true);
|
|
const sequenceGuid = String(context.sequence.guid || "");
|
|
if (input.expectedSequenceGuid && input.expectedSequenceGuid !== sequenceGuid) {
|
|
throw commandError("UXP_STALE_SEQUENCE", "The active sequence changed before the lift request; inspect the project and retry with its current sequence GUID");
|
|
}
|
|
const selection = await context.sequence.getSelection();
|
|
if (!selection || typeof selection.getTrackItems !== "function") {
|
|
throw commandError("UXP_COMMAND_UNAVAILABLE", "This Premiere build does not expose timeline item selection for UXP lift");
|
|
}
|
|
const selectedItems = Array.from(await selection.getTrackItems() || []);
|
|
if (!selectedItems.length) throw commandError("UXP_INVALID_ARGUMENT", "Select one or more timeline items before requesting a lift");
|
|
const editor = await ppro.SequenceEditor.getEditor(context.sequence);
|
|
const mediaType = ppro.Constants && ppro.Constants.MediaType && ppro.Constants.MediaType.ANY;
|
|
if (!editor || typeof editor.createRemoveItemsAction !== "function" || mediaType == null) {
|
|
throw commandError("UXP_COMMAND_UNAVAILABLE", "This Premiere build does not expose the documented SequenceEditor lift action");
|
|
}
|
|
let committed = false;
|
|
context.project.lockedAccess(() => {
|
|
const action = editor.createRemoveItemsAction(selection, false, mediaType, false);
|
|
committed = context.project.executeTransaction((compoundAction) => {
|
|
if (!action || compoundAction.addAction(action) === false) throw commandError("UXP_ACTION_REJECTED", "Premiere rejected the selection lift action");
|
|
}, "Lift selected timeline items");
|
|
});
|
|
assertTransactionCommitted(committed, "selection lift");
|
|
return {
|
|
lifted: true, selectedItemCount: selectedItems.length, ripple: false, outcome: "committed_unverified",
|
|
verificationBoundary: "project_executeTransaction_return",
|
|
operation: operationSemantics({
|
|
mutatesProject: true, verificationStatus: "not_verified", verificationBoundary: "project_executeTransaction_return",
|
|
verificationEvidence: [{ type: "transaction", accepted: true, selectedItemCount: selectedItems.length }],
|
|
undoSupported: true, undoLabel: "Lift selected timeline items", transactionActionGroup: true, cancellationSupported: true
|
|
})
|
|
};
|
|
}
|
|
async function listTransitions() {
|
|
const matchNames = await currentVideoTransitionMatchNames();
|
|
return { matchNames, count: matchNames.length };
|
|
}
|
|
async function inspectTransition(args) {
|
|
const input = validateInspectTransitionArgs(args), context = await activeContext(false);
|
|
return {
|
|
target: await transitionTargetSnapshot(context.sequence, input.videoTrackIndex, input.clipIndex, input.position),
|
|
verificationBoundary: "video_transition_target_readback"
|
|
};
|
|
}
|
|
async function addTransition(args) {
|
|
const input = validateAddArgs(args);
|
|
const duration = input.durationSeconds == null ? null : await tickTime(input.durationSeconds, "durationSeconds");
|
|
return withTransitionUpdateLock(input.expectedTarget.sequenceGuid, async () => {
|
|
const context = await guardedTransitionContext(input), target = context.target;
|
|
if (context.before.transitionPresent) {
|
|
throw commandError("UXP_STALE_TRANSITION_TARGET", "A transition is already present at the requested edge; inspect and retry");
|
|
}
|
|
const available = await currentVideoTransitionMatchNames();
|
|
if (!available.includes(input.matchName) && !listedVideoTransitionMatchNames.has(input.matchName)) {
|
|
throw commandError("UXP_TRANSITION_NOT_FOUND", "Unknown video transition matchName: " + input.matchName);
|
|
}
|
|
const transition = await ppro.TransitionFactory.createVideoTransition(input.matchName);
|
|
if (!transition) throw commandError("UXP_TRANSITION_NOT_FOUND", "Premiere could not create video transition: " + input.matchName);
|
|
const options = ppro.AddTransitionOptions();
|
|
options.setApplyToStart(input.position === "start");
|
|
if (duration) options.setDuration(duration);
|
|
if (input.forceSingleSided != null) options.setForceSingleSided(input.forceSingleSided);
|
|
if (input.transitionAlignment != null) options.setTransitionAlignment(input.transitionAlignment);
|
|
let committed = false;
|
|
context.project.lockedAccess(() => {
|
|
const action = target.createAddVideoTransitionAction(transition, options);
|
|
committed = context.project.executeTransaction((compoundAction) => {
|
|
if (!action || compoundAction.addAction(action) === false) throw commandError("UXP_ACTION_REJECTED", "Premiere rejected the transition action");
|
|
}, "Add video transition");
|
|
});
|
|
assertTransactionCommitted(committed, "transition addition");
|
|
const after = await transitionTargetSnapshot(context.sequence, input.videoTrackIndex, input.clipIndex, input.position);
|
|
if (!after.transitionPresent) throw commandError("UXP_VERIFICATION_FAILED", "Premiere committed the transition transaction but no transition is present at the requested edge");
|
|
return verifiedTransitionResult(true, input, after, "Add video transition");
|
|
});
|
|
}
|
|
async function currentVideoTransitionMatchNames() {
|
|
const matchNames = Array.from(await ppro.TransitionFactory.getVideoTransitionMatchNames() || []);
|
|
for (let i = 0; i < matchNames.length; i += 1) {
|
|
if (typeof matchNames[i] === "string" && matchNames[i]) listedVideoTransitionMatchNames.add(matchNames[i]);
|
|
}
|
|
return matchNames;
|
|
}
|
|
async function removeTransition(args) {
|
|
const input = validateRemoveArgs(args);
|
|
return withTransitionUpdateLock(input.expectedTarget.sequenceGuid, async () => {
|
|
const context = await guardedTransitionContext(input), target = context.target;
|
|
if (!context.before.transitionPresent) {
|
|
throw commandError("UXP_STALE_TRANSITION_TARGET", "No transition is present at the requested edge; inspect and retry");
|
|
}
|
|
let committed = false;
|
|
context.project.lockedAccess(() => {
|
|
const action = target.createRemoveVideoTransitionAction(context.positionValue);
|
|
committed = context.project.executeTransaction((compoundAction) => {
|
|
if (!action || compoundAction.addAction(action) === false) throw commandError("UXP_ACTION_REJECTED", "Premiere rejected the transition action");
|
|
}, "Remove video transition");
|
|
});
|
|
assertTransactionCommitted(committed, "transition removal");
|
|
const after = await transitionTargetSnapshot(context.sequence, input.videoTrackIndex, input.clipIndex, input.position);
|
|
if (after.transitionPresent) throw commandError("UXP_VERIFICATION_FAILED", "Premiere committed the transition transaction but the requested edge is still occupied");
|
|
return verifiedTransitionResult(false, input, after, "Remove video transition");
|
|
});
|
|
}
|
|
async function guardedTransitionContext(input) {
|
|
const context = await activeContext(true), sequenceGuid = String(context.sequence.guid || "");
|
|
if (!sequenceGuid || sequenceGuid !== input.expectedTarget.sequenceGuid) {
|
|
throw commandError("UXP_STALE_SEQUENCE", "The active sequence changed before the transition update; inspect and retry");
|
|
}
|
|
const positionValue = transitionPositionValue(input.position);
|
|
const target = await videoClipAt(context.sequence, input.videoTrackIndex, input.clipIndex);
|
|
const before = await transitionTargetSnapshot(context.sequence, input.videoTrackIndex, input.clipIndex, input.position, target, positionValue);
|
|
assertExpectedTransitionTarget(before, input.expectedTarget);
|
|
return { ...context, target, before, positionValue };
|
|
}
|
|
async function transitionTargetSnapshot(sequence, videoTrackIndex, clipIndex, position, target, positionValue) {
|
|
const item = target || await videoClipAt(sequence, videoTrackIndex, clipIndex);
|
|
const edge = positionValue == null ? transitionPositionValue(position) : positionValue;
|
|
if (typeof item.getProjectItem !== "function" || typeof item.getStartTime !== "function" || typeof item.getEndTime !== "function" || typeof item.hasVideoTransition !== "function") {
|
|
throw commandError("UXP_COMMAND_UNAVAILABLE", "This Premiere build cannot read a complete video-transition target snapshot");
|
|
}
|
|
const projectItem = await item.getProjectItem();
|
|
if (!projectItem || typeof projectItem.getId !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "This Premiere build cannot identify the selected video clip");
|
|
const projectItemId = await projectItem.getId();
|
|
const startSeconds = tickSecondsRequired(await item.getStartTime(), "video transition target start"), endSeconds = tickSecondsRequired(await item.getEndTime(), "video transition target end");
|
|
if (typeof projectItemId !== "string" || !projectItemId || endSeconds < startSeconds) {
|
|
throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not return a complete video-transition target snapshot");
|
|
}
|
|
const transitionPresent = await item.hasVideoTransition(edge);
|
|
if (typeof transitionPresent !== "boolean") throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not return a boolean video-transition edge state");
|
|
const sequenceGuid = String(sequence && sequence.guid || "");
|
|
if (!sequenceGuid) throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not provide a stable active-sequence GUID");
|
|
return { sequenceGuid, videoTrackIndex, clipIndex, projectItemId, startSeconds, endSeconds, position, transitionPresent };
|
|
}
|
|
function transitionPositionValue(position) {
|
|
const positions = ppro.Constants && ppro.Constants.TransitionPosition, value = positions && positions[String(position).toUpperCase()];
|
|
if (value == null) throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere transition position constants are unavailable");
|
|
return value;
|
|
}
|
|
function assertExpectedTransitionTarget(actual, expected) {
|
|
if (actual.sequenceGuid !== expected.sequenceGuid || actual.videoTrackIndex !== expected.videoTrackIndex || actual.clipIndex !== expected.clipIndex ||
|
|
actual.projectItemId !== expected.projectItemId || !sameSeconds(actual.startSeconds, expected.startSeconds) || !sameSeconds(actual.endSeconds, expected.endSeconds) ||
|
|
actual.position !== expected.position || actual.transitionPresent !== expected.transitionPresent) {
|
|
throw commandError("UXP_STALE_TRANSITION_TARGET", "The video-transition target changed before the update; inspect and retry");
|
|
}
|
|
}
|
|
function verifiedTransitionResult(applied, input, target, undoLabel) {
|
|
return {
|
|
...(applied ? { applied: true, matchName: input.matchName } : { removed: true }),
|
|
outcome: "verified", verified: "video_transition_edge_readback", target,
|
|
operation: operationSemantics({
|
|
mutatesProject: true, verificationStatus: "verified", verificationBoundary: "video_transition_edge_readback",
|
|
verificationEvidence: [{ type: "video_transition_edge", target }], undoSupported: true,
|
|
undoLabel, transactionActionGroup: true, cancellationSupported: false
|
|
})
|
|
};
|
|
}
|
|
function withTransitionUpdateLock(sequenceGuid, operation) {
|
|
const previous = transitionUpdateTails.get(sequenceGuid) || Promise.resolve();
|
|
let release;
|
|
const gate = new Promise((resolve) => { release = resolve; });
|
|
const tail = previous.catch(() => undefined).then(() => gate);
|
|
transitionUpdateTails.set(sequenceGuid, tail);
|
|
return previous.catch(() => undefined).then(operation).finally(() => {
|
|
release();
|
|
if (transitionUpdateTails.get(sequenceGuid) === tail) transitionUpdateTails.delete(sequenceGuid);
|
|
});
|
|
}
|
|
async function activeSequence(project) {
|
|
const sequence = await project.getActiveSequence();
|
|
if (!sequence) throw commandError("UXP_NO_ACTIVE_SEQUENCE", "No active sequence");
|
|
return sequence;
|
|
}
|
|
async function trackAt(sequence, trackType, trackIndex) {
|
|
const title = trackLabel(trackType), countMethod = "get" + title + "Count", itemMethod = "get" + title;
|
|
if (typeof sequence[countMethod] !== "function" || typeof sequence[itemMethod] !== "function") {
|
|
throw commandError("UXP_COMMAND_UNAVAILABLE", "This Premiere build does not expose " + title + " APIs");
|
|
}
|
|
const count = await sequence[countMethod]();
|
|
if (trackIndex >= count) throw commandError("UXP_TARGET_NOT_FOUND", trackType + " trackIndex " + trackIndex + " is out of range");
|
|
const track = await sequence[itemMethod](trackIndex);
|
|
if (!track) throw commandError("UXP_TARGET_NOT_FOUND", trackType + " trackIndex " + trackIndex + " was not found");
|
|
return track;
|
|
}
|
|
function trackLabel(trackType) {
|
|
return trackType === "audio" ? "AudioTrack" : trackType === "video" ? "VideoTrack" : "CaptionTrack";
|
|
}
|
|
async function resolveClipProjectItem(project, input) {
|
|
const wantedId = input.projectItemId || "", wantedName = input.projectItemName || "";
|
|
if (!wantedId && !wantedName) return selectedClipProjectItem(project);
|
|
if (typeof project.getRootItem !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "This Premiere build cannot enumerate project items");
|
|
const root = await project.getRootItem();
|
|
const queue = root ? [root] : [], nameMatches = [];
|
|
while (queue.length) {
|
|
const folder = queue.shift();
|
|
if (!folder || typeof folder.getItems !== "function") continue;
|
|
const children = Array.from(await folder.getItems() || []);
|
|
for (let i = 0; i < children.length; i += 1) {
|
|
const item = children[i], itemId = await projectItemIdentifier(item);
|
|
if (wantedId && itemId === wantedId) return castClipProjectItem(item);
|
|
if (!wantedId && wantedName && String(item.name || "") === wantedName) {
|
|
try { nameMatches.push(castClipProjectItem(item)); } catch (_) {}
|
|
}
|
|
if (isFolderItem(item)) queue.push(item);
|
|
}
|
|
}
|
|
if (wantedId) throw commandError("UXP_TARGET_NOT_FOUND", "projectItemId was not found or is not a media clip");
|
|
if (nameMatches.length > 1) throw commandError("UXP_AMBIGUOUS_TARGET", "projectItemName matched multiple media clips; use projectItemId");
|
|
if (nameMatches.length === 1) return nameMatches[0];
|
|
throw commandError("UXP_TARGET_NOT_FOUND", "projectItemName was not found or is not a media clip");
|
|
}
|
|
async function selectedClipProjectItem(project) {
|
|
if (!ppro.ProjectUtils || typeof ppro.ProjectUtils.getSelection !== "function") {
|
|
throw commandError("UXP_INVALID_ARGUMENT", "Pass projectItemId or projectItemName because Project panel selection is unavailable");
|
|
}
|
|
const selection = await ppro.ProjectUtils.getSelection(project);
|
|
const items = selection && await selection.getItems();
|
|
if (!items || items.length !== 1) {
|
|
throw commandError("UXP_INVALID_ARGUMENT", "Select exactly one media project item, or pass projectItemId/projectItemName");
|
|
}
|
|
return castClipProjectItem(items[0]);
|
|
}
|
|
function castClipProjectItem(item) {
|
|
if (!ppro.ClipProjectItem || typeof ppro.ClipProjectItem.cast !== "function") {
|
|
throw commandError("UXP_COMMAND_UNAVAILABLE", "This Premiere build cannot cast project items to media clips");
|
|
}
|
|
try {
|
|
const clip = ppro.ClipProjectItem.cast(item);
|
|
if (clip) return clip;
|
|
} catch (_) {}
|
|
throw commandError("UXP_TARGET_NOT_FOUND", "Resolved project item is not a media clip");
|
|
}
|
|
function isFolderItem(item) {
|
|
if (!ppro.FolderItem || typeof ppro.FolderItem.cast !== "function") return false;
|
|
try { return !!ppro.FolderItem.cast(item); } catch (_) { return false; }
|
|
}
|
|
async function projectItemIdentifier(item) {
|
|
let projectItem = item;
|
|
if (ppro.ProjectItem && typeof ppro.ProjectItem.cast === "function") {
|
|
try { projectItem = ppro.ProjectItem.cast(item) || item; } catch (_) {}
|
|
}
|
|
if (!projectItem || typeof projectItem.getId !== "function") return "";
|
|
const id = await projectItem.getId();
|
|
return id == null ? "" : String(id);
|
|
}
|
|
async function projectItemInventory(project) {
|
|
if (typeof project.getRootItem !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "This Premiere build cannot enumerate project items for subclip verification");
|
|
const root = await project.getRootItem(), queue = root ? [root] : [], items = [];
|
|
while (queue.length) {
|
|
const folder = queue.shift();
|
|
if (!folder || typeof folder.getItems !== "function") continue;
|
|
const children = Array.from(await folder.getItems() || []);
|
|
for (let i = 0; i < children.length; i += 1) {
|
|
const item = children[i];
|
|
items.push({ id: await projectItemIdentifier(item), name: String(item.name || ""), item });
|
|
if (isFolderItem(item)) queue.push(item);
|
|
}
|
|
}
|
|
return items;
|
|
}
|
|
async function findCreatedSubclip(project, before, name) {
|
|
const known = new Set(before.map((item) => item.id).filter(Boolean));
|
|
const after = await projectItemInventory(project);
|
|
for (let i = 0; i < after.length; i += 1) {
|
|
const item = after[i];
|
|
if (item.name !== name || !item.id || known.has(item.id)) continue;
|
|
try {
|
|
const clip = castClipProjectItem(item.item);
|
|
return { projectItemId: item.id, name: String(clip.name || item.name) };
|
|
} catch (_) {}
|
|
}
|
|
return null;
|
|
}
|
|
async function markerSnapshot(marker, includeWebLinks, includeColorValues) {
|
|
const start = await marker.getStart(), duration = await marker.getDuration();
|
|
const snapshot = {
|
|
guid: stringifyGuid(marker.guid), name: String(await marker.getName() || ""),
|
|
comments: String(await marker.getComments() || ""), type: String(await marker.getType() || ""),
|
|
colorIndex: await marker.getColorIndex(), startSeconds: tickSeconds(start), durationSeconds: tickSeconds(duration)
|
|
};
|
|
if (includeWebLinks) {
|
|
snapshot.url = await optionalMarkerString(marker, "getUrl");
|
|
snapshot.target = await optionalMarkerString(marker, "getTarget");
|
|
}
|
|
if (includeColorValues) snapshot.color = await optionalMarkerColor(marker);
|
|
return snapshot;
|
|
}
|
|
async function optionalMarkerString(marker, method) {
|
|
if (!marker || typeof marker[method] !== "function") return null;
|
|
const value = await marker[method]();
|
|
return value == null ? "" : String(value);
|
|
}
|
|
async function optionalMarkerColor(marker) {
|
|
if (!marker || typeof marker.getColor !== "function") return null;
|
|
const color = await marker.getColor();
|
|
if (color == null) return null;
|
|
const snapshot = { red: color.red, green: color.green, blue: color.blue, alpha: color.alpha };
|
|
if (!Object.values(snapshot).every((value) => typeof value === "number" && Number.isFinite(value))) {
|
|
throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not return finite marker color components");
|
|
}
|
|
return snapshot;
|
|
}
|
|
function stringifyGuid(value) {
|
|
if (value == null) return "";
|
|
try { return String(typeof value.toString === "function" ? value.toString() : value); } catch (_) { return ""; }
|
|
}
|
|
function tickSeconds(value) {
|
|
const seconds = value && Number(value.seconds);
|
|
return Number.isFinite(seconds) ? seconds : null;
|
|
}
|
|
async function videoClipAt(sequence, trackIndex, clipIndex) {
|
|
const trackCount = await sequence.getVideoTrackCount();
|
|
if (trackIndex >= trackCount) throw commandError("UXP_TARGET_NOT_FOUND", "videoTrackIndex " + trackIndex + " is out of range");
|
|
const track = await sequence.getVideoTrack(trackIndex), types = ppro.Constants && ppro.Constants.TrackItemType;
|
|
if (!track || !types || types.CLIP == null) throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere video track APIs are unavailable");
|
|
const clips = await track.getTrackItems(types.CLIP, false);
|
|
if (!clips || !clips[clipIndex]) throw commandError("UXP_TARGET_NOT_FOUND", "clipIndex " + clipIndex + " is out of range on video track " + trackIndex);
|
|
return clips[clipIndex];
|
|
}
|
|
function assertTransactionCommitted(committed, operation) {
|
|
if (!committed) throw commandError("UXP_TRANSACTION_FAILED", "Premiere did not commit the " + operation + " transaction");
|
|
}
|
|
function operationSemantics(options) {
|
|
return Protocol && typeof Protocol.operationSemantics === "function" ? Protocol.operationSemantics(options) : undefined;
|
|
}
|
|
async function allowedPath(value, label, kind) {
|
|
const path = requiredString(value, label);
|
|
return workspace && typeof workspace.assertPathAllowed === "function"
|
|
? await workspace.assertPathAllowed(path, { label, kind })
|
|
: path;
|
|
}
|
|
function canExportFrame() { return !!(ppro.Exporter && typeof ppro.Exporter.exportSequenceFrame === "function"); }
|
|
function canLiftSelection() {
|
|
return !!(ppro.SequenceEditor && typeof ppro.SequenceEditor.getEditor === "function" &&
|
|
ppro.Constants && ppro.Constants.MediaType && ppro.Constants.MediaType.ANY != null) && activeSequenceHas(["getSelection"]);
|
|
}
|
|
function canInspectProject() { return !!(ppro.Project && typeof ppro.Project.getActiveProject === "function"); }
|
|
async function activeProjectHas(name) {
|
|
if (!canInspectProject()) return false;
|
|
const project = await ppro.Project.getActiveProject();
|
|
return !project || typeof project[name] === "function";
|
|
}
|
|
function canSaveProject() { return activeProjectHas("save"); }
|
|
function canInspectProjectInsertionBin() { return activeProjectHas("getInsertionBin"); }
|
|
function canInspectInstalledMogrtDirectory() {
|
|
return !!(ppro.SequenceEditor && typeof ppro.SequenceEditor.getInstalledMogrtPath === "function");
|
|
}
|
|
function canCreatePresetSequence() { return activeProjectHas("createSequenceWithPresetPath"); }
|
|
function canInspectSequenceRange() {
|
|
return activeSequenceHas(["getInPoint", "getOutPoint", "getZeroPoint", "getEndTime"]);
|
|
}
|
|
function canInspectSequencePlayhead() {
|
|
return activeSequenceHas(["getPlayerPosition"]);
|
|
}
|
|
function canInspectFrameAlignment() {
|
|
return !!(ppro.FrameRate && typeof ppro.FrameRate.createWithValue === "function" &&
|
|
ppro.TickTime && typeof ppro.TickTime.createWithSeconds === "function" &&
|
|
typeof ppro.TickTime.createWithFrameAndFrameRate === "function");
|
|
}
|
|
function canInspectSequenceTiming() {
|
|
return activeSequenceHas([
|
|
"getFrameSize", "getTimebase", "getSequenceAudioTimeDisplayFormat",
|
|
"getSequenceVideoTimeDisplayFormat", "getProjectItem"
|
|
]);
|
|
}
|
|
async function canInspectSequenceTimingByGuid() {
|
|
if (!canInspectProject() || !ppro.Guid || typeof ppro.Guid.fromString !== "function") return false;
|
|
const project = await ppro.Project.getActiveProject();
|
|
return !project || typeof project.getSequence === "function";
|
|
}
|
|
function canInspectAppPreferences() {
|
|
if (!ppro.AppPreference || typeof ppro.AppPreference.getValue !== "function") return false;
|
|
return Object.values(APP_PREFERENCE_KEYS).every((property) => typeof ppro.AppPreference[property] === "string" && ppro.AppPreference[property]);
|
|
}
|
|
function canSetAppPreferences() {
|
|
return canInspectAppPreferences() && typeof ppro.AppPreference.setValue === "function" &&
|
|
Number.isFinite(ppro.AppPreference.PROPERTY_PERSISTENT) && Number.isFinite(ppro.AppPreference.PROPERTY_NON_PERSISTENT);
|
|
}
|
|
async function canSetSequencePlayhead() {
|
|
return !!(ppro.TickTime && typeof ppro.TickTime.createWithSeconds === "function" &&
|
|
await canInspectSequencePlayhead() && await activeSequenceHas(["setPlayerPosition"]));
|
|
}
|
|
async function canUpdateSequenceRange() {
|
|
if (!ppro.TickTime || typeof ppro.TickTime.createWithSeconds !== "function" || !await canInspectSequenceRange()) return false;
|
|
const project = await ppro.Project.getActiveProject();
|
|
if (!project) return true;
|
|
if (typeof project.lockedAccess !== "function" || typeof project.executeTransaction !== "function") return false;
|
|
const sequence = await project.getActiveSequence();
|
|
return !sequence || ["createSetInPointAction", "createSetOutPointAction", "createSetZeroPointAction"]
|
|
.every((method) => typeof sequence[method] === "function");
|
|
}
|
|
function canExportInterchange() {
|
|
return !!(ppro.ProjectConverter && typeof ppro.ProjectConverter.exportAsOpenTimelineIO === "function" &&
|
|
typeof ppro.ProjectConverter.exportAsFinalCutProXML === "function");
|
|
}
|
|
function canExportAaf() {
|
|
return !!(ppro.ProjectConverter && typeof ppro.ProjectConverter.exportAAF === "function" &&
|
|
typeof ppro.AAFExportOptions === "function");
|
|
}
|
|
async function activeSequenceHas(methods) {
|
|
if (!canInspectProject()) return false;
|
|
const project = await ppro.Project.getActiveProject();
|
|
if (!project) return true;
|
|
const sequence = await project.getActiveSequence();
|
|
return !sequence || methods.every((method) => typeof sequence[method] === "function");
|
|
}
|
|
function canRenameTracks() {
|
|
return activeSequenceHas(["getAudioTrackCount", "getAudioTrack", "getVideoTrackCount", "getVideoTrack", "getCaptionTrackCount", "getCaptionTrack"]);
|
|
}
|
|
function canCreateSubclips() {
|
|
return canInspectProject() && !!(ppro.ClipProjectItem && typeof ppro.ClipProjectItem.cast === "function");
|
|
}
|
|
function canListMarkers() { return !!(ppro.Markers && typeof ppro.Markers.getMarkers === "function"); }
|
|
function canSetSourceMonitorPosition() {
|
|
return !!(ppro.SourceMonitor && typeof ppro.SourceMonitor.setPosition === "function" && typeof ppro.SourceMonitor.getPosition === "function" &&
|
|
ppro.TickTime && typeof ppro.TickTime.createWithSeconds === "function");
|
|
}
|
|
function canQueryTranscriptLanguages() { return !!(ppro.Transcript && typeof ppro.Transcript.querySupportedLanguages === "function"); }
|
|
function canInspectObjectMasks() { return !!(ppro.ObjectMaskUtils && typeof ppro.ObjectMaskUtils.hasObjectMask === "function"); }
|
|
function canConfigureEncoder() {
|
|
return !!(ppro.EncoderManager && typeof ppro.EncoderManager.launchEncoder === "function" &&
|
|
typeof ppro.EncoderManager.startBatchEncode === "function");
|
|
}
|
|
function canListTransitions() { return !!(ppro.TransitionFactory && typeof ppro.TransitionFactory.getVideoTransitionMatchNames === "function"); }
|
|
function canInspectTransitions() {
|
|
return activeSequenceHas(["getVideoTrackCount", "getVideoTrack"]);
|
|
}
|
|
async function canMutateTransitions() {
|
|
if (!canListTransitions() || typeof ppro.TransitionFactory.createVideoTransition !== "function" ||
|
|
typeof ppro.AddTransitionOptions !== "function" || !ppro.TickTime || typeof ppro.TickTime.createWithSeconds !== "function" ||
|
|
!(ppro.Constants && ppro.Constants.TrackItemType && ppro.Constants.TransitionPosition) || !await canInspectTransitions()) return false;
|
|
const project = await ppro.Project.getActiveProject();
|
|
return !project || (typeof project.lockedAccess === "function" && typeof project.executeTransaction === "function");
|
|
}
|
|
async function initialize() {
|
|
return nextWorkflowRuntime && typeof nextWorkflowRuntime.initialize === "function"
|
|
? nextWorkflowRuntime.initialize()
|
|
: { initialized: true };
|
|
}
|
|
async function dispose() {
|
|
return nextWorkflowRuntime && typeof nextWorkflowRuntime.dispose === "function"
|
|
? nextWorkflowRuntime.dispose()
|
|
: { disposed: true };
|
|
}
|
|
return { definitions, dispatch, capabilities, stateSnapshot, initialize, dispose };
|
|
}
|
|
|
|
function validateAddArgs(args) {
|
|
assertObject(args);
|
|
assertOnlyKeys(args, ["videoTrackIndex", "clipIndex", "matchName", "position", "durationSeconds", "forceSingleSided", "transitionAlignment", "expectedTarget", "operationId"]);
|
|
const result = targetArgs(args);
|
|
if (typeof args.matchName !== "string" || !args.matchName.trim() || args.matchName.length > 256) throw commandError("UXP_INVALID_ARGUMENT", "matchName must be a non-empty string of at most 256 characters");
|
|
result.matchName = args.matchName; result.position = optionalPosition(args.position);
|
|
result.expectedTarget = validateExpectedTransitionTarget(args.expectedTarget, result);
|
|
if (args.durationSeconds != null) result.durationSeconds = positiveNumber(args.durationSeconds, "durationSeconds");
|
|
if (args.forceSingleSided != null) {
|
|
if (typeof args.forceSingleSided !== "boolean") throw commandError("UXP_INVALID_ARGUMENT", "forceSingleSided must be a boolean");
|
|
result.forceSingleSided = args.forceSingleSided;
|
|
}
|
|
if (args.transitionAlignment != null) {
|
|
if (!Number.isInteger(args.transitionAlignment)) throw commandError("UXP_INVALID_ARGUMENT", "transitionAlignment must be an integer");
|
|
result.transitionAlignment = args.transitionAlignment;
|
|
}
|
|
return result;
|
|
}
|
|
function validateRemoveArgs(args) {
|
|
assertObject(args); assertOnlyKeys(args, ["videoTrackIndex", "clipIndex", "position", "expectedTarget", "operationId"]);
|
|
const result = targetArgs(args); result.position = optionalPosition(args.position); result.expectedTarget = validateExpectedTransitionTarget(args.expectedTarget, result); return result;
|
|
}
|
|
function validateInspectTransitionArgs(args) {
|
|
assertObject(args); assertOnlyKeys(args, ["videoTrackIndex", "clipIndex", "position"]);
|
|
const result = targetArgs(args); result.position = optionalPosition(args.position); return result;
|
|
}
|
|
function validateExpectedTransitionTarget(value, target) {
|
|
assertObject(value);
|
|
assertOnlyKeys(value, ["sequenceGuid", "videoTrackIndex", "clipIndex", "projectItemId", "startSeconds", "endSeconds", "position", "transitionPresent"]);
|
|
const sequenceGuid = boundedString(value.sequenceGuid, "expectedTarget.sequenceGuid", 512);
|
|
const projectItemId = boundedString(value.projectItemId, "expectedTarget.projectItemId", 512);
|
|
const videoTrackIndex = nonNegativeInt(value.videoTrackIndex, "expectedTarget.videoTrackIndex"), clipIndex = nonNegativeInt(value.clipIndex, "expectedTarget.clipIndex");
|
|
if (videoTrackIndex !== target.videoTrackIndex || clipIndex !== target.clipIndex) {
|
|
throw commandError("UXP_INVALID_ARGUMENT", "expectedTarget coordinates must match videoTrackIndex and clipIndex");
|
|
}
|
|
const position = optionalPosition(value.position);
|
|
if (position !== target.position) throw commandError("UXP_INVALID_ARGUMENT", "expectedTarget.position must match position");
|
|
const startSeconds = boundedTransitionSeconds(value.startSeconds, "expectedTarget.startSeconds"), endSeconds = boundedTransitionSeconds(value.endSeconds, "expectedTarget.endSeconds");
|
|
if (endSeconds < startSeconds) throw commandError("UXP_INVALID_ARGUMENT", "expectedTarget.endSeconds must be greater than or equal to startSeconds");
|
|
return { sequenceGuid, videoTrackIndex, clipIndex, projectItemId, startSeconds, endSeconds, position, transitionPresent: requiredBoolean(value.transitionPresent, "expectedTarget.transitionPresent") };
|
|
}
|
|
function validateLiftArgs(args) {
|
|
assertObject(args); assertOnlyKeys(args, ["expectedSequenceGuid", "operationId"]);
|
|
const result = {};
|
|
if (args.expectedSequenceGuid != null) result.expectedSequenceGuid = boundedString(args.expectedSequenceGuid, "expectedSequenceGuid", 512);
|
|
return result;
|
|
}
|
|
function validateRenameTrackArgs(args) {
|
|
assertObject(args); assertOnlyKeys(args, ["trackType", "trackIndex", "name", "operationId"]);
|
|
const trackType = args.trackType;
|
|
if (trackType !== "audio" && trackType !== "video" && trackType !== "caption") {
|
|
throw commandError("UXP_INVALID_ARGUMENT", "trackType must be audio, video, or caption");
|
|
}
|
|
return { trackType, trackIndex: nonNegativeInt(args.trackIndex, "trackIndex"), name: boundedString(args.name, "name", 255) };
|
|
}
|
|
function validateSubclipArgs(args) {
|
|
assertObject(args);
|
|
assertOnlyKeys(args, ["projectItemId", "projectItemName", "name", "startSeconds", "endSeconds", "hasHardBoundaries", "takeVideo", "takeAudio", "operationId"]);
|
|
const target = validateProjectItemTarget(args);
|
|
const startSeconds = nonNegativeNumber(args.startSeconds, "startSeconds"), endSeconds = nonNegativeNumber(args.endSeconds, "endSeconds");
|
|
if (endSeconds <= startSeconds) throw commandError("UXP_INVALID_ARGUMENT", "endSeconds must be greater than startSeconds");
|
|
const hasHardBoundaries = optionalBoolean(args.hasHardBoundaries, false, "hasHardBoundaries");
|
|
const takeVideo = optionalBoolean(args.takeVideo, true, "takeVideo"), takeAudio = optionalBoolean(args.takeAudio, true, "takeAudio");
|
|
if (!takeVideo && !takeAudio) throw commandError("UXP_INVALID_ARGUMENT", "At least one of takeVideo or takeAudio must be true");
|
|
return Object.assign(target, {
|
|
name: boundedString(args.name, "name", 255), startSeconds, endSeconds, hasHardBoundaries, takeVideo, takeAudio
|
|
});
|
|
}
|
|
function validateAppPreferenceSetArgs(args) {
|
|
assertObject(args);
|
|
assertOnlyKeys(args, ["preference", "expectedValue", "value", "persistence", "confirmPreferenceChange", "operationId"]);
|
|
const preference = args.preference;
|
|
if (preference !== "auto_peak_generation" && preference !== "import_workspace" && preference !== "show_quickstart_dialog") {
|
|
throw commandError("UXP_INVALID_ARGUMENT", "preference must be auto_peak_generation, import_workspace, or show_quickstart_dialog");
|
|
}
|
|
if (args.confirmPreferenceChange !== true) {
|
|
throw commandError("UXP_CONFIRMATION_REQUIRED", "App preference changes are direct application-state updates; pass confirmPreferenceChange=true after review");
|
|
}
|
|
const persistence = args.persistence;
|
|
if (persistence !== "persistent" && persistence !== "non_persistent") {
|
|
throw commandError("UXP_INVALID_ARGUMENT", "persistence must be persistent or non_persistent");
|
|
}
|
|
return {
|
|
preference,
|
|
expectedValue: boundedStringAllowEmpty(args.expectedValue, "expectedValue", 1024),
|
|
value: boundedStringAllowEmpty(args.value, "value", 1024),
|
|
persistence,
|
|
operationId: requiredOperationId(args.operationId)
|
|
};
|
|
}
|
|
function validateMarkerListArgs(args) {
|
|
assertObject(args); assertOnlyKeys(args, ["scope", "projectItemId", "projectItemName", "filters", "includeWebLinks", "includeColorValues"]);
|
|
const scope = args.scope == null ? "sequence" : args.scope;
|
|
if (scope !== "sequence" && scope !== "projectItem") throw commandError("UXP_INVALID_ARGUMENT", "scope must be sequence or projectItem");
|
|
if (scope === "sequence" && (args.projectItemId != null || args.projectItemName != null)) {
|
|
throw commandError("UXP_INVALID_ARGUMENT", "project item targeting is only valid for projectItem scope");
|
|
}
|
|
const target = scope === "projectItem" ? validateProjectItemTarget(args) : {};
|
|
let filters = [];
|
|
if (args.filters != null) {
|
|
if (!Array.isArray(args.filters) || args.filters.length > 16) throw commandError("UXP_INVALID_ARGUMENT", "filters must contain at most 16 marker types");
|
|
filters = args.filters.map((value, index) => boundedString(value, "filters[" + index + "]", 64));
|
|
}
|
|
return Object.assign({
|
|
scope, filters,
|
|
includeWebLinks: optionalBoolean(args.includeWebLinks, false, "includeWebLinks"),
|
|
includeColorValues: optionalBoolean(args.includeColorValues, false, "includeColorValues")
|
|
}, target);
|
|
}
|
|
function validateProjectItemTarget(args) {
|
|
const hasId = args.projectItemId != null, hasName = args.projectItemName != null;
|
|
if (hasId && hasName) throw commandError("UXP_INVALID_ARGUMENT", "Pass either projectItemId or projectItemName, not both");
|
|
const result = {};
|
|
if (hasId) result.projectItemId = boundedString(args.projectItemId, "projectItemId", 512);
|
|
if (hasName) result.projectItemName = boundedString(args.projectItemName, "projectItemName", 255);
|
|
return result;
|
|
}
|
|
function validateAafArgs(args) {
|
|
assertObject(args); assertOnlyKeys(args, ["outputFilePath", "options", "operationId"]);
|
|
const result = { outputFilePath: requiredString(args.outputFilePath, "outputFilePath"), options: {} };
|
|
if (result.outputFilePath.length > 4096) throw commandError("UXP_INVALID_ARGUMENT", "outputFilePath must be at most 4096 characters");
|
|
if (args.options == null) return result;
|
|
assertObject(args.options);
|
|
const value = args.options;
|
|
assertOnlyKeys(value, [
|
|
"mixdownVideo", "explodeToMono", "sampleRate", "bitsPerSample", "embedAudio", "audioFileFormat",
|
|
"trimSources", "handleFrames", "videoMixdownPresetPath", "renderAudioEffects", "interleaveWithoutEffects", "preserveParentFolder"
|
|
]);
|
|
const booleans = ["mixdownVideo", "explodeToMono", "embedAudio", "trimSources", "renderAudioEffects", "interleaveWithoutEffects", "preserveParentFolder"];
|
|
for (let i = 0; i < booleans.length; i += 1) {
|
|
const key = booleans[i];
|
|
if (value[key] != null) result.options[key] = requiredBoolean(value[key], key);
|
|
}
|
|
if (value.sampleRate != null) result.options.sampleRate = oneOfInt(value.sampleRate, "sampleRate", [32000, 44100, 48000, 88200, 96000]);
|
|
if (value.bitsPerSample != null) result.options.bitsPerSample = oneOfInt(value.bitsPerSample, "bitsPerSample", [16, 24, 32]);
|
|
if (value.audioFileFormat != null) {
|
|
if (value.audioFileFormat !== "aiff" && value.audioFileFormat !== "wav") throw commandError("UXP_INVALID_ARGUMENT", "audioFileFormat must be aiff or wav");
|
|
result.options.audioFileFormat = value.audioFileFormat;
|
|
}
|
|
if (value.handleFrames != null) {
|
|
const frames = nonNegativeInt(value.handleFrames, "handleFrames");
|
|
if (frames > 10000) throw commandError("UXP_INVALID_ARGUMENT", "handleFrames must be at most 10000");
|
|
result.options.handleFrames = frames;
|
|
}
|
|
if (value.videoMixdownPresetPath != null) result.options.videoMixdownPresetPath = boundedString(value.videoMixdownPresetPath, "videoMixdownPresetPath", 4096);
|
|
return result;
|
|
}
|
|
function buildAafExportOptions(ppro, options) {
|
|
if (!ppro.AAFExportOptions || typeof ppro.AAFExportOptions !== "function") {
|
|
throw commandError("UXP_COMMAND_UNAVAILABLE", "This Premiere build does not expose AAFExportOptions");
|
|
}
|
|
const aaf = ppro.AAFExportOptions();
|
|
const setters = {
|
|
mixdownVideo: "setMixdownVideo", explodeToMono: "setExplodeToMono", sampleRate: "setSampleRate",
|
|
bitsPerSample: "setBitsPerSample", embedAudio: "setEmbedAudio", trimSources: "setTrimSources",
|
|
handleFrames: "setHandleFrames", videoMixdownPresetPath: "setVideoMixdownPresetPath",
|
|
renderAudioEffects: "setRenderAudioEffects", interleaveWithoutEffects: "setInterleaveWithoutEffects",
|
|
preserveParentFolder: "setPreserveParentFolder"
|
|
};
|
|
Object.keys(setters).forEach((key) => {
|
|
if (options[key] == null) return;
|
|
const setter = setters[key];
|
|
if (typeof aaf[setter] !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "AAF option " + key + " is unavailable in this Premiere build");
|
|
aaf[setter](options[key]);
|
|
});
|
|
if (options.audioFileFormat != null) {
|
|
const formats = ppro.Constants && ppro.Constants.AAFExportAudioFormat;
|
|
const format = formats && formats[options.audioFileFormat === "aiff" ? "AIFF" : "WAV"];
|
|
if (format == null || typeof aaf.setAudioFileFormat !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "AAF audio file format options are unavailable in this Premiere build");
|
|
aaf.setAudioFileFormat(format);
|
|
}
|
|
return aaf;
|
|
}
|
|
function targetArgs(args) { return { videoTrackIndex: nonNegativeInt(args.videoTrackIndex, "videoTrackIndex"), clipIndex: nonNegativeInt(args.clipIndex, "clipIndex") }; }
|
|
function optionalPosition(value) { const result = value == null ? "end" : value; if (result !== "start" && result !== "end") throw commandError("UXP_INVALID_ARGUMENT", "position must be start or end"); return result; }
|
|
function assertObject(value) { if (!value || typeof value !== "object" || Array.isArray(value)) throw commandError("UXP_INVALID_ARGUMENT", "args must be an object"); }
|
|
function assertOnlyKeys(value, allowed) { const unknown = Object.keys(value).filter((key) => !allowed.includes(key)); if (unknown.length) throw commandError("UXP_INVALID_ARGUMENT", "Unknown argument: " + unknown[0]); }
|
|
function nonNegativeInt(value, name) { if (!Number.isInteger(value) || value < 0) throw commandError("UXP_INVALID_ARGUMENT", name + " must be a non-negative integer"); return value; }
|
|
function nonNegativeNumber(value, name) { const number = Number(value); if (!Number.isFinite(number) || number < 0) throw commandError("UXP_INVALID_ARGUMENT", name + " must be a non-negative number"); return number; }
|
|
function boundedTransitionSeconds(value, name) { const number = nonNegativeNumber(value, name); if (number > 86400) throw commandError("UXP_INVALID_ARGUMENT", name + " must be at most 86400"); return number; }
|
|
function positiveNumber(value, name) { const number = Number(value); if (!Number.isFinite(number) || number <= 0) throw commandError("UXP_INVALID_ARGUMENT", name + " must be positive"); return number; }
|
|
function positiveInt(value, fallback, name) { return Math.round(positiveNumber(value == null ? fallback : value, name)); }
|
|
function requiredString(value, name) { if (typeof value !== "string" || !value.trim() || value.length > 4096) throw commandError("UXP_INVALID_ARGUMENT", name + " must be a non-empty string"); return value; }
|
|
function boundedString(value, name, maximum) { if (typeof value !== "string" || !value.trim() || value.length > maximum) throw commandError("UXP_INVALID_ARGUMENT", name + " must be a non-empty string of at most " + maximum + " characters"); return value; }
|
|
function boundedStringAllowEmpty(value, name, maximum) { if (typeof value !== "string" || value.length > maximum) throw commandError("UXP_INVALID_ARGUMENT", name + " must be a string of at most " + maximum + " characters"); return value; }
|
|
function requiredBoolean(value, name) { if (typeof value !== "boolean") throw commandError("UXP_INVALID_ARGUMENT", name + " must be a boolean"); return value; }
|
|
function optionalBoolean(value, fallback, name) { return value == null ? fallback : requiredBoolean(value, name); }
|
|
function oneOfInt(value, name, allowed) { if (!Number.isInteger(value) || !allowed.includes(value)) throw commandError("UXP_INVALID_ARGUMENT", name + " must be one of " + allowed.join(", ")); return value; }
|
|
function requiredOperationId(value) { if (typeof value !== "string" || !/^[A-Za-z0-9._:-]{1,128}$/.test(value)) throw commandError("UXP_INVALID_ARGUMENT", "operationId is required and must be 1-128 safe characters"); return value; }
|
|
function validateOperationId(value) { if (value == null) return null; if (typeof value !== "string" || !/^[A-Za-z0-9._:-]{1,128}$/.test(value)) throw commandError("UXP_INVALID_ARGUMENT", "operationId must be 1-128 safe characters"); return value; }
|
|
function simpleRevision(value) { var hash = 2166136261; for (var i = 0; i < value.length; i += 1) { hash ^= value.charCodeAt(i); hash = Math.imul(hash, 16777619); } return "uxp-" + (hash >>> 0).toString(16); }
|
|
function commandError(code, message) { const error = new Error(message); error.code = code; return error; }
|
|
return { createCommandRegistry, validateAddArgs, validateRemoveArgs, validateInspectTransitionArgs, commandError };
|
|
});
|