(function (root, factory) { const api = factory(); if (typeof module === "object" && module.exports) module.exports = api; root.PremiereMcpWorkflows = api; })(typeof globalThis !== "undefined" ? globalThis : this, function () { "use strict"; const MAX_SELECTION_ITEMS = 64; const MAX_METADATA_CHARS = 350000; const MAX_METADATA_RESULT_BYTES = 900000; // A Project-panel schema is XML and can expand materially when represented // in the JSON bridge request (quotes and backslashes are escaped). Keep each // exact stale guard and replacement far below the protocol ceiling instead // of accepting a large pair that cannot be safely serialized together. const MAX_PROJECT_PANEL_METADATA_UPDATE_BYTES = 12 * 1024; function utf8ByteLength(value) { let bytes = 0; for (let i = 0; i < value.length; i += 1) { const code = value.charCodeAt(i); if (code < 0x80) bytes += 1; else if (code < 0x800) bytes += 2; else if (code >= 0xD800 && code <= 0xDBFF && i + 1 < value.length && value.charCodeAt(i + 1) >= 0xDC00 && value.charCodeAt(i + 1) <= 0xDFFF) { bytes += 4; i += 1; } else bytes += 3; } return bytes; } function createWorkflowDefinitions(deps) { const ppro = deps.ppro, Protocol = deps.Protocol, workspace = deps.workspace; const projectPanelMetadataMutationTails = new Map(); const definitions = { "effects.catalog": { readOnly: true, minHostVersion: "25.6.0", probe: canUseEffects, handler: effectCatalog }, "effects.chain.get": { readOnly: true, targetCapabilityProbe: true, minHostVersion: "25.6.0", probe: canUseEffects, handler: effectChain }, "trackItem.identity.inspect": { readOnly: true, targetCapabilityProbe: true, minHostVersion: "25.6.0", probe: canInspectTrackItemIdentity, handler: inspectTrackItemIdentity }, "effects.chain.add": { destructive: true, undoable: true, targetCapabilityProbe: true, minHostVersion: "25.6.0", probe: canUseEffects, handler: addEffect }, "effects.chain.remove": { destructive: true, undoable: true, targetCapabilityProbe: true, minHostVersion: "25.6.0", probe: canUseEffects, handler: removeEffect }, "selection.inspect": { readOnly: true, minHostVersion: "25.6.0", probe: canUseSelection, handler: inspectSelection }, "selection.fingerprints.inspect": { readOnly: true, minHostVersion: "25.6.0", probe: canUseSelection, handler: inspectSelectionFingerprints }, "selection.targets.inspect": { readOnly: true, targetCapabilityProbe: true, minHostVersion: "25.6.0", probe: canUseSelection, handler: inspectSelectionTargets }, "selection.update": { idempotent: true, minHostVersion: "25.6.0", probe: canManageSelection, handler: updateSelection }, "effects.selection.add": { destructive: true, undoable: true, targetCapabilityProbe: true, minHostVersion: "25.6.0", probe: canUseEffectsSelection, handler: addEffectToSelection }, "effects.selection.remove": { destructive: true, undoable: true, targetCapabilityProbe: true, minHostVersion: "25.6.0", probe: canUseEffectsSelection, handler: removeEffectFromSelection }, "sceneEdit.detect": { destructive: true, undoable: false, minHostVersion: "26.3.0", probe: canDetectScenes, handler: detectScenes }, "proxy.inspect": { readOnly: true, targetCapabilityProbe: true, minHostVersion: "25.6.0", probe: canUseClipItems, handler: inspectProxy }, "proxy.attach": { destructive: true, undoable: false, idempotent: true, targetCapabilityProbe: true, requiresWorkspace: true, minHostVersion: "25.6.0", probe: canAttachProxy, handler: attachProxy }, "ingest.get": { readOnly: true, minHostVersion: "25.6.0", probe: canUseIngest, handler: getIngest }, "ingest.configure": { destructive: true, undoable: true, idempotent: true, minHostVersion: "25.6.0", probe: canUseIngest, handler: configureIngest }, "media.relink": { destructive: true, undoable: false, idempotent: true, targetCapabilityProbe: true, requiresWorkspace: true, minHostVersion: "25.6.0", probe: canRelink, handler: relinkMedia }, "metadata.get": { readOnly: true, minHostVersion: "25.6.0", probe: canUseMetadata, handler: getMetadata }, "metadata.update": { destructive: true, undoable: true, idempotent: true, minHostVersion: "25.6.0", probe: canUseMetadata, handler: updateMetadata }, "metadata.columns.get": { readOnly: true, targetCapabilityProbe: true, minHostVersion: "25.6.0", probe: canGetProjectColumnsMetadata, handler: getProjectColumnsMetadata }, "metadata.projectPanel.get": { readOnly: true, minHostVersion: "25.6.0", probe: canGetProjectPanelMetadata, handler: getProjectPanelMetadata }, "metadata.projectPanel.update": { destructive: true, undoable: false, idempotent: true, targetCapabilityProbe: true, minHostVersion: "25.6.0", probe: canSetProjectPanelMetadata, handler: updateProjectPanelMetadata }, "metadata.projectSchema.inspect": { readOnly: true, minHostVersion: "25.6.0", probe: canCreateProjectMetadataSchema, handler: inspectProjectMetadataSchema }, "metadata.projectSchema.create": { destructive: true, undoable: false, idempotent: true, targetCapabilityProbe: true, minHostVersion: "25.6.0", probe: canCreateProjectMetadataSchema, handler: createProjectMetadataField }, "color.preflight": { readOnly: true, targetCapabilityProbe: true, minHostVersion: "25.6.0", probe: canInspectColor, handler: colorPreflight }, "environment.inspect": { readOnly: true, targetCapabilityProbe: true, minHostVersion: "25.6.0", probe: canInspectEnvironment, handler: inspectEnvironment }, "footage.conform": { destructive: true, undoable: true, idempotent: true, targetCapabilityProbe: true, minHostVersion: "25.6.0", probe: canConformFootage, handler: conformFootage }, "sourceMonitor.state": { readOnly: true, minHostVersion: "25.6.0", probe: canInspectSourceMonitor, handler: sourceMonitorState }, "sourceMonitor.open": { idempotent: true, conditionalWorkspace: true, minHostVersion: "25.6.0", probe: canOpenSourceMonitor, handler: openSourceMonitor }, "sourceMonitor.play": { minHostVersion: "25.6.0", probe: canPlaySourceMonitor, handler: playSourceMonitor }, "sourceMonitor.close": { idempotent: true, minHostVersion: "25.6.0", probe: canCloseSourceMonitor, handler: closeSourceMonitor }, "storage.preflight": { readOnly: true, minHostVersion: "25.6.0", probe: canUseProjectSettings, handler: storagePreflight }, "scratch.configure": { destructive: true, undoable: true, idempotent: true, minHostVersion: "25.6.0", probe: canConfigureScratch, handler: configureScratch }, "workspace.status": { readOnly: true, minHostVersion: "25.6.0", probe: canReportWorkspace, handler: workspaceStatus } }; async function activeProject(requireTransactions) { const project = await ppro.Project.getActiveProject(); if (!project) throw commandError("UXP_NO_ACTIVE_PROJECT", "No active project"); if (requireTransactions && (typeof project.lockedAccess !== "function" || typeof project.executeTransaction !== "function")) { throw commandError("UXP_COMMAND_UNAVAILABLE", "This Premiere build does not expose locked undoable transactions"); } return project; } async function activeContext(requireTransactions) { const project = await activeProject(requireTransactions); const sequence = await project.getActiveSequence(); if (!sequence) throw commandError("UXP_NO_ACTIVE_SEQUENCE", "No active sequence"); return { project, sequence }; } 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(), 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 index = 0; index < children.length; index += 1) { const item = children[index], 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), 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 castProjectItem(item) { if (!ppro.ProjectItem || typeof ppro.ProjectItem.cast !== "function") return item; try { return ppro.ProjectItem.cast(item) || item; } catch (_) { return item; } } 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) { const projectItem = castProjectItem(item); if (!projectItem || typeof projectItem.getId !== "function") return ""; const id = await projectItem.getId(); return id == null ? "" : String(id); } async function clipTarget(args, allowedKeys) { assertObject(args); assertOnlyKeys(args, allowedKeys); const target = validateProjectItemTarget(args); const project = await activeProject(false); return { project, clip: await resolveClipProjectItem(project, target), target }; } async function trackItemAt(sequence, mediaType, trackIndex, clipIndex, trackItemsByTrack) { const cacheKey = mediaType + ":" + trackIndex; if (trackItemsByTrack && trackItemsByTrack.has(cacheKey)) { const cachedItems = trackItemsByTrack.get(cacheKey); if (!cachedItems[clipIndex]) throw commandError("UXP_TARGET_NOT_FOUND", "clipIndex " + clipIndex + " is out of range on " + mediaType + " track " + trackIndex); return cachedItems[clipIndex]; } const title = mediaType === "video" ? "Video" : "Audio"; const countMethod = "get" + title + "TrackCount", trackMethod = "get" + title + "Track"; if (typeof sequence[countMethod] !== "function" || typeof sequence[trackMethod] !== "function") { throw commandError("UXP_COMMAND_UNAVAILABLE", "This Premiere build does not expose " + mediaType + " track APIs"); } const count = await sequence[countMethod](); if (trackIndex >= count) throw commandError("UXP_TARGET_NOT_FOUND", mediaType + " trackIndex " + trackIndex + " is out of range"); const track = await sequence[trackMethod](trackIndex), itemType = ppro.Constants && ppro.Constants.TrackItemType; if (!track || !itemType || itemType.CLIP == null || typeof track.getTrackItems !== "function") { throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere clip track-item APIs are unavailable"); } const items = Array.from(await track.getTrackItems(itemType.CLIP, false) || []); if (trackItemsByTrack) trackItemsByTrack.set(cacheKey, items); if (!items[clipIndex]) throw commandError("UXP_TARGET_NOT_FOUND", "clipIndex " + clipIndex + " is out of range on " + mediaType + " track " + trackIndex); return items[clipIndex]; } async function currentTrackItems(sequence, requireNonEmpty, enforceLimit) { if (typeof sequence.getSelection !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "This Premiere build cannot inspect the timeline selection"); const selection = await sequence.getSelection(); if (!selection || typeof selection.getTrackItems !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere did not return a track-item selection"); const items = Array.from(await selection.getTrackItems() || []); if (requireNonEmpty && !items.length) throw commandError("UXP_EMPTY_SELECTION", "Select at least one clip in the active sequence"); if (enforceLimit !== false && items.length > MAX_SELECTION_ITEMS) { throw commandError("UXP_SELECTION_TOO_LARGE", "Select at most " + MAX_SELECTION_ITEMS + " clips per compound operation"); } return { selection, items }; } async function selectedTrackItems(sequence) { return currentTrackItems(sequence, true); } async function classifySelection(sequence, selectedItems) { const cache = { video: new Map(), audio: new Map() }, classified = []; async function clipsAt(mediaType, trackIndex) { const values = cache[mediaType]; if (values.has(trackIndex)) return values.get(trackIndex); const title = mediaType === "video" ? "Video" : "Audio", trackMethod = "get" + title + "Track"; const itemType = ppro.Constants && ppro.Constants.TrackItemType; let items = []; try { const track = typeof sequence[trackMethod] === "function" ? await sequence[trackMethod](trackIndex) : null; if (track && itemType && itemType.CLIP != null && typeof track.getTrackItems === "function") { items = Array.from(await track.getTrackItems(itemType.CLIP, false) || []); } } catch (_) {} values.set(trackIndex, items); return items; } for (let selectionIndex = 0; selectionIndex < selectedItems.length; selectionIndex += 1) { const item = selectedItems[selectionIndex]; let trackIndex = null, mediaType = "unknown", clipIndex = null; try { trackIndex = typeof item.getTrackIndex === "function" ? await item.getTrackIndex() : null; } catch (_) {} if (Number.isInteger(trackIndex) && trackIndex >= 0) { const video = await clipsAt("video", trackIndex), videoIndex = video.indexOf(item); if (videoIndex >= 0) { mediaType = "video"; clipIndex = videoIndex; } else { const audio = await clipsAt("audio", trackIndex), audioIndex = audio.indexOf(item); if (audioIndex >= 0) { mediaType = "audio"; clipIndex = audioIndex; } } } classified.push({ item, selectionIndex, mediaType, trackIndex, clipIndex }); } return classified; } async function componentChain(item) { if (!item || typeof item.getComponentChain !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "The selected clip does not expose an effect component chain"); const chain = await item.getComponentChain(); if (!chain || typeof chain.getComponentCount !== "function" || typeof chain.getComponentAtIndex !== "function") { throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere did not return a documented effect component chain"); } return chain; } async function componentInfo(component, index) { let matchName = "", displayName = "", parameterCount = null; try { if (component && typeof component.getMatchName === "function") matchName = String(await component.getMatchName() || ""); } catch (_) {} try { if (component && typeof component.getDisplayName === "function") displayName = String(await component.getDisplayName() || ""); } catch (_) {} try { if (component && typeof component.getParamCount === "function") parameterCount = component.getParamCount(); } catch (_) {} return { index, matchName, displayName, parameterCount }; } async function chainSnapshot(chain) { const count = chain.getComponentCount(), components = []; for (let index = 0; index < count; index += 1) components.push(await componentInfo(chain.getComponentAtIndex(index), index)); return { count, components }; } async function effectCatalog(args) { assertObject(args); assertOnlyKeys(args, ["mediaType"]); const mediaType = args.mediaType == null ? "all" : enumValue(args.mediaType, "mediaType", ["video", "audio", "all"]); const result = {}; if (mediaType === "video" || mediaType === "all") { if (!ppro.VideoFilterFactory || typeof ppro.VideoFilterFactory.getMatchNames !== "function" || typeof ppro.VideoFilterFactory.getDisplayNames !== "function") { if (mediaType === "video") throw commandError("UXP_COMMAND_UNAVAILABLE", "Video effect catalog APIs are unavailable"); } else { result.video = { matchNames: Array.from(await ppro.VideoFilterFactory.getMatchNames() || []), displayNames: Array.from(await ppro.VideoFilterFactory.getDisplayNames() || []) }; } } if (mediaType === "audio" || mediaType === "all") { if (!ppro.AudioFilterFactory || typeof ppro.AudioFilterFactory.getDisplayNames !== "function") { if (mediaType === "audio") throw commandError("UXP_COMMAND_UNAVAILABLE", "Audio effect catalog APIs are unavailable"); } else result.audio = { displayNames: Array.from(await ppro.AudioFilterFactory.getDisplayNames() || []) }; } return result; } async function effectChain(args) { const input = validateTrackTarget(args, ["mediaType", "trackIndex", "clipIndex"]), context = await activeContext(false); const item = await trackItemAt(context.sequence, input.mediaType, input.trackIndex, input.clipIndex); return { ...input, ...(await chainSnapshot(await componentChain(item))) }; } async function inspectTrackItemIdentity(args) { const input = validateTrackItemIdentityArgs(args), context = await activeContext(false); const sequenceGuid = guidString(context.sequence && context.sequence.guid); if (!sequenceGuid) throw commandError("UXP_COMMAND_UNAVAILABLE", "The active sequence does not expose a stable GUID"); if (input.expectedSequenceGuid && input.expectedSequenceGuid !== sequenceGuid) { throw commandError("UXP_STALE_SEQUENCE", "The active sequence differs from expectedSequenceGuid; inspect the track-item identity again"); } const item = await trackItemAt(context.sequence, input.mediaType, input.trackIndex, input.clipIndex); const identity = await trackItemIdentitySnapshot(item); const activeAfter = await context.project.getActiveSequence(), activeAfterGuid = guidString(activeAfter && activeAfter.guid); if (activeAfterGuid !== sequenceGuid) { throw commandError("UXP_STALE_SEQUENCE", "The active sequence changed while reading the track-item identity; retry the inspection"); } return { sequenceGuid, mediaType: input.mediaType, trackIndex: input.trackIndex, clipIndex: input.clipIndex, ...identity, verificationBoundary: "active_sequence_identity_readback" }; } async function trackItemIdentitySnapshot(item) { const required = ["getMatchName", "getType", "getMediaType", "getTrackIndex", "getIsSelected"]; const missing = required.find((name) => !item || typeof item[name] !== "function"); if (missing) throw commandError("UXP_COMMAND_UNAVAILABLE", "This track item does not expose documented " + missing + " identity access"); const matchName = await item.getMatchName(), trackItemType = await item.getType(), mediaTypeGuid = guidString(await item.getMediaType()); const reportedTrackIndex = await item.getTrackIndex(), selected = await item.getIsSelected(); if (typeof matchName !== "string" || matchName.length > 512 || !Number.isInteger(trackItemType) || !mediaTypeGuid || !Number.isInteger(reportedTrackIndex) || reportedTrackIndex < 0 || typeof selected !== "boolean") { throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an incomplete track-item identity snapshot"); } return { matchName, trackItemType, mediaTypeGuid, reportedTrackIndex, selected }; } function guidString(value) { if (value == null) return ""; try { const result = String(typeof value.toString === "function" ? value.toString() : value); return result.length <= 512 ? result : ""; } catch (_) { return ""; } } async function createEffectComponent(mediaType, effectId, item) { if (mediaType === "video") { const available = Array.from(await ppro.VideoFilterFactory.getMatchNames() || []); if (!available.includes(effectId)) throw commandError("UXP_EFFECT_NOT_FOUND", "Unknown video effect matchName: " + effectId); const component = await ppro.VideoFilterFactory.createComponent(effectId); if (!component) throw commandError("UXP_EFFECT_NOT_FOUND", "Premiere could not create video effect: " + effectId); return component; } const available = Array.from(await ppro.AudioFilterFactory.getDisplayNames() || []); if (!available.includes(effectId)) throw commandError("UXP_EFFECT_NOT_FOUND", "Unknown audio effect display name: " + effectId); const component = await ppro.AudioFilterFactory.createComponentByDisplayName(effectId, item); if (!component) throw commandError("UXP_EFFECT_NOT_FOUND", "Premiere could not create audio effect: " + effectId); return component; } async function addEffect(args) { const input = validateEffectAdd(args, false), context = await activeContext(true); const item = await trackItemAt(context.sequence, input.mediaType, input.trackIndex, input.clipIndex); const chain = await componentChain(item), before = chain.getComponentCount(); if (input.insertionIndex != null && input.insertionIndex > before) throw commandError("UXP_INVALID_ARGUMENT", "insertionIndex exceeds the component count"); const component = await createEffectComponent(input.mediaType, input.effectId, item); let committed = false; context.project.lockedAccess(() => { const action = input.insertionIndex == null ? chain.createAppendComponentAction(component) : chain.createInsertComponentAction(component, input.insertionIndex); committed = context.project.executeTransaction((compoundAction) => { if (!action || compoundAction.addAction(action) === false) throw commandError("UXP_ACTION_REJECTED", "Premiere rejected the effect action"); }, "Add " + input.mediaType + " effect"); }); assertCommitted(committed, "effect addition"); const after = await chainSnapshot(chain), verified = after.count === before + 1; return mutationResult(verified, { applied: true, mediaType: input.mediaType, trackIndex: input.trackIndex, clipIndex: input.clipIndex, effectId: input.effectId, insertionIndex: input.insertionIndex, beforeCount: before, after }, "effect_chain_count_readback", "Add " + input.mediaType + " effect"); } async function removeEffect(args) { const input = validateEffectRemove(args, false), context = await activeContext(true); const item = await trackItemAt(context.sequence, input.mediaType, input.trackIndex, input.clipIndex); const chain = await componentChain(item), before = chain.getComponentCount(); if (input.componentIndex >= before) throw commandError("UXP_TARGET_NOT_FOUND", "componentIndex is out of range"); const component = chain.getComponentAtIndex(input.componentIndex); await assertExpectedComponent(component, input.expectedEffectId); let committed = false; context.project.lockedAccess(() => { const action = chain.createRemoveComponentAction(component); committed = context.project.executeTransaction((compoundAction) => { if (!action || compoundAction.addAction(action) === false) throw commandError("UXP_ACTION_REJECTED", "Premiere rejected the effect removal action"); }, "Remove " + input.mediaType + " effect"); }); assertCommitted(committed, "effect removal"); const after = await chainSnapshot(chain), verified = after.count === before - 1; return mutationResult(verified, { removed: true, mediaType: input.mediaType, trackIndex: input.trackIndex, clipIndex: input.clipIndex, componentIndex: input.componentIndex, expectedEffectId: input.expectedEffectId, beforeCount: before, after }, "effect_chain_count_readback", "Remove " + input.mediaType + " effect"); } async function inspectSelection(args) { assertObject(args); assertOnlyKeys(args, []); const context = await activeContext(false), selected = await selectedTrackItems(context.sequence); const classified = await classifySelection(context.sequence, selected.items), items = []; for (const value of classified) items.push(await selectionItemSnapshot(value)); return { count: items.length, items }; } async function inspectSelectionFingerprints(args) { assertObject(args); assertOnlyKeys(args, []); const context = await activeContext(false), selected = await currentTrackItems(context.sequence, false); const classified = await classifySelection(context.sequence, selected.items), items = []; assertClassifiedSelection(classified, "The current selection contains an item that cannot be addressed safely"); for (const value of classified) items.push(await selectionFingerprintSnapshot(value, true)); return { sequenceGuid: activeSequenceGuid(context.sequence), count: items.length, items }; } async function inspectSelectionTargets(args) { const inputs = validateSelectionTargetInspectionArgs(args), context = await activeContext(false), items = []; const trackItemsByTrack = new Map(); for (let index = 0; index < inputs.length; index += 1) { const input = inputs[index], item = await trackItemAt(context.sequence, input.mediaType, input.trackIndex, input.clipIndex, trackItemsByTrack); const snapshot = await selectionFingerprintSnapshot({ item, selectionIndex: index, mediaType: input.mediaType, trackIndex: input.trackIndex, clipIndex: input.clipIndex }); items.push({ targetIndex: index, mediaType: snapshot.mediaType, trackIndex: snapshot.trackIndex, clipIndex: snapshot.clipIndex, name: snapshot.name, startSeconds: snapshot.startSeconds, endSeconds: snapshot.endSeconds, projectItem: snapshot.projectItem }); } return { sequenceGuid: activeSequenceGuid(context.sequence), count: items.length, items }; } async function selectionFingerprintSnapshot(value, includeComponentCount) { const item = value.item; if (!item || typeof item.getProjectItem !== "function" || typeof item.getStartTime !== "function" || typeof item.getEndTime !== "function") { throw commandError("UXP_SELECTION_FINGERPRINT_UNAVAILABLE", "Timeline item " + value.selectionIndex + " does not expose a complete mutation fingerprint"); } let projectItem = null, projectItemId = "", startSeconds = null, endSeconds = null; try { projectItem = await item.getProjectItem(); projectItemId = await projectItemIdentifier(projectItem); startSeconds = tickSeconds(await item.getStartTime()); endSeconds = tickSeconds(await item.getEndTime()); } catch (_) { throw commandError("UXP_SELECTION_FINGERPRINT_UNAVAILABLE", "Timeline item " + value.selectionIndex + " could not provide its project item and native timeline times"); } if (!projectItemId || startSeconds == null || endSeconds == null) { throw commandError("UXP_SELECTION_FINGERPRINT_UNAVAILABLE", "Timeline item " + value.selectionIndex + " returned an incomplete mutation fingerprint"); } let componentCount = null; if (includeComponentCount) { try { componentCount = (await componentChain(item)).getComponentCount(); } catch (_) {} } return { selectionIndex: value.selectionIndex, mediaType: value.mediaType, trackIndex: value.trackIndex, clipIndex: value.clipIndex, name: String(item.name || ""), startSeconds, endSeconds, componentCount, projectItem: { id: projectItemId, name: String(projectItem && projectItem.name || "") } }; } async function selectionItemSnapshot(value) { let projectItem = null, startSeconds = null, endSeconds = null, componentCount = null; try { if (typeof value.item.getProjectItem === "function") { const item = await value.item.getProjectItem(); projectItem = { id: await projectItemIdentifier(item), name: String(item && item.name || "") }; } } catch (_) {} try { startSeconds = tickSeconds(await value.item.getStartTime()); } catch (_) { try { startSeconds = tickSeconds(await value.item.getInPoint()); } catch (_) {} } try { endSeconds = tickSeconds(await value.item.getEndTime()); } catch (_) { try { endSeconds = tickSeconds(await value.item.getOutPoint()); } catch (_) {} } try { componentCount = (await componentChain(value.item)).getComponentCount(); } catch (_) {} return { selectionIndex: value.selectionIndex, mediaType: value.mediaType, trackIndex: value.trackIndex, clipIndex: value.clipIndex, name: String(value.item.name || ""), startSeconds, endSeconds, componentCount, projectItem }; } async function updateSelection(args) { const input = validateSelectionUpdateArgs(args), context = await activeContext(false); const sequenceGuid = activeSequenceGuid(context.sequence); if (!sequenceGuid || input.expectedSequenceGuid !== sequenceGuid) { throw commandError("UXP_STALE_SEQUENCE", "The active sequence changed; inspect the timeline selection again before updating it"); } await planSelectionUpdate(context.sequence, input); const mutationContext = await activeContext(false), mutationSequenceGuid = activeSequenceGuid(mutationContext.sequence); if (!mutationSequenceGuid || input.expectedSequenceGuid !== mutationSequenceGuid) { throw commandError("UXP_STALE_SEQUENCE", "The active sequence changed; inspect the timeline selection again before updating it"); } await planSelectionUpdate(mutationContext.sequence, input); const commitContext = await activeContext(false), commitSequenceGuid = activeSequenceGuid(commitContext.sequence); if (!commitSequenceGuid || input.expectedSequenceGuid !== commitSequenceGuid) { throw commandError("UXP_STALE_SEQUENCE", "The active sequence changed; inspect the timeline selection again before updating it"); } const commitPlan = await planSelectionUpdate(commitContext.sequence, input); if (input.mode === "add" || input.mode === "remove") { const currentBase = await selectionSnapshot(commitContext.sequence, false); const plannedKeys = commitPlan.before ? selectionSnapshotKeys(commitPlan.before.items) : []; if (!commitPlan.before || !sameStringArrays(plannedKeys, selectionSnapshotKeys(currentBase.items))) { throw commandError("UXP_STALE_SELECTION", "The current timeline selection changed while the update was being prepared; inspect it again before updating it"); } } const finalContext = await activeContext(false), finalSequenceGuid = activeSequenceGuid(finalContext.sequence); if (!finalSequenceGuid || input.expectedSequenceGuid !== finalSequenceGuid) { throw commandError("UXP_STALE_SEQUENCE", "The active sequence changed; inspect the timeline selection again before updating it"); } const plan = await planSelectionUpdate(finalContext.sequence, input); const beforeSelection = plan.beforeSelection, before = plan.before; const desiredItems = plan.desiredItems, desired = plan.desired; if (input.mode === "add" || input.mode === "remove") { const commitKeys = commitPlan.before ? selectionSnapshotKeys(commitPlan.before.items) : []; const finalKeys = before ? selectionSnapshotKeys(before.items) : []; const currentBase = await selectionSnapshot(finalContext.sequence, false); const currentKeys = selectionSnapshotKeys(currentBase.items); if (!commitPlan.before || !before || !sameStringArrays(commitKeys, finalKeys) || !sameStringArrays(finalKeys, currentKeys)) { throw commandError("UXP_STALE_SELECTION", "The current timeline selection changed while the update was being prepared; inspect it again before updating it"); } } const verifiedContext = await activeContext(false), verifiedSequenceGuid = activeSequenceGuid(verifiedContext.sequence); if (!verifiedSequenceGuid || input.expectedSequenceGuid !== verifiedSequenceGuid) { throw commandError("UXP_STALE_SEQUENCE", "The active sequence changed; inspect the timeline selection again before updating it"); } if (desiredItems.length) { const selection = createEmptyTrackItemSelection(); for (const item of desiredItems) { if (selection.addItem(item, false) !== true) { throw commandError("UXP_SELECTION_REJECTED", "Premiere rejected a clip while constructing the timeline selection"); } } const set = await hostBoolean(verifiedContext.sequence.setSelection(selection)); if (!set) throw commandError("UXP_SELECTION_REJECTED", "Premiere did not accept the requested timeline selection"); } else { const cleared = await hostBoolean(verifiedContext.sequence.clearSelection()); if (!cleared) throw commandError("UXP_SELECTION_REJECTED", "Premiere did not clear the timeline selection"); } const after = await selectionSnapshot(verifiedContext.sequence); assertClassifiedSelection(after.classified, "Premiere returned an unclassified timeline item after the selection update"); const expectedKeys = selectionSnapshotKeys(desired.items), actualKeys = selectionSnapshotKeys(after.items); if (!sameStringArrays(expectedKeys, actualKeys)) { throw commandError("UXP_VERIFICATION_FAILED", "The timeline selection readback did not match the requested clips"); } const beforeKeys = before ? selectionSnapshotKeys(before.items) : null; const changed = beforeKeys ? !sameStringArrays(beforeKeys, actualKeys) : input.mode === "clear" ? beforeSelection.items.length > 0 : true; return { updated: true, changed, mode: input.mode, sequenceGuid, count: after.items.length, items: after.items, outcome: "verified", verified: "timeline_selection_readback", verificationBoundary: "timeline_selection_readback", operation: operationSemantics({ mutatesProject: false, verificationStatus: "verified", verificationBoundary: "timeline_selection_readback", verificationEvidence: [{ type: "timeline_selection", sequenceGuid, count: after.items.length }], cancellationSupported: true }) }; } async function planSelectionUpdate(sequence, input) { const beforeSelection = await currentTrackItems(sequence, false, false); let before = null; let desiredItems = []; if (input.mode !== "clear") { const resolved = await resolveSelectionTargets(sequence, input.items); if (input.mode === "replace") { desiredItems = resolved.map((value) => value.item); if (beforeSelection.items.length <= MAX_SELECTION_ITEMS) { before = await itemSelectionSnapshot(sequence, beforeSelection.items); assertClassifiedSelection(before.classified, "The current selection contains an item that cannot be addressed safely"); } } else { if (input.mode === "add" && beforeSelection.items.length > MAX_SELECTION_ITEMS) { throw commandError("UXP_SELECTION_TOO_LARGE", "The updated selection would exceed " + MAX_SELECTION_ITEMS + " clips"); } if (input.mode === "remove" && beforeSelection.items.length - resolved.length > MAX_SELECTION_ITEMS) { throw commandError("UXP_SELECTION_TOO_LARGE", "The updated selection would exceed " + MAX_SELECTION_ITEMS + " clips"); } before = await itemSelectionSnapshot(sequence, beforeSelection.items); assertClassifiedSelection(before.classified, "The current selection contains an item that cannot be addressed safely"); desiredItems = before.classified.map((value) => value.item); if (input.mode === "add") { const existing = new Set(before.classified.map(selectionCoordinateKey)); for (const value of resolved) { const coordinate = selectionCoordinateKey(value.snapshot); if (!existing.has(coordinate)) { desiredItems.push(value.item); existing.add(coordinate); } } if (desiredItems.length > MAX_SELECTION_ITEMS) { throw commandError("UXP_SELECTION_TOO_LARGE", "The updated selection would exceed " + MAX_SELECTION_ITEMS + " clips"); } } else { const removed = new Set(resolved.map((value) => selectionCoordinateKey(value.snapshot))); desiredItems = before.classified .filter((value) => !removed.has(selectionCoordinateKey(value))) .map((value) => value.item); } } } if (desiredItems.length > MAX_SELECTION_ITEMS) { throw commandError("UXP_SELECTION_TOO_LARGE", "The updated selection would exceed " + MAX_SELECTION_ITEMS + " clips"); } const desired = await itemSelectionSnapshot(sequence, desiredItems); assertClassifiedSelection(desired.classified, "Premiere could not classify a requested timeline item"); return { beforeSelection, before, desiredItems, desired }; } async function resolveSelectionTargets(sequence, inputs) { const result = [], trackItemsByTrack = new Map(); for (let index = 0; index < inputs.length; index += 1) { const input = inputs[index], item = await trackItemAt(sequence, input.mediaType, input.trackIndex, input.clipIndex, trackItemsByTrack); const snapshot = await selectionFingerprintSnapshot({ item, selectionIndex: index, mediaType: input.mediaType, trackIndex: input.trackIndex, clipIndex: input.clipIndex }); const projectItemId = snapshot.projectItem && snapshot.projectItem.id; if (projectItemId !== input.expectedProjectItemId || !valuesEqual(snapshot.startSeconds, input.expectedStartSeconds) || !valuesEqual(snapshot.endSeconds, input.expectedEndSeconds)) { throw commandError("UXP_STALE_SELECTION_TARGET", "Timeline item " + index + " changed; inspect the selection again before updating it"); } result.push({ item, snapshot }); } return result; } async function selectionSnapshot(sequence, enforceLimit) { const selected = await currentTrackItems(sequence, false, enforceLimit); return itemSelectionSnapshot(sequence, selected.items); } async function itemSelectionSnapshot(sequence, selectedItems) { const classified = await classifySelection(sequence, selectedItems), items = []; for (const value of classified) items.push(await selectionFingerprintSnapshot(value)); return { classified, items }; } function assertClassifiedSelection(classified, message) { if (classified.some((value) => value.mediaType !== "video" && value.mediaType !== "audio" || !Number.isInteger(value.trackIndex) || !Number.isInteger(value.clipIndex))) { throw commandError("UXP_UNCLASSIFIED_SELECTION", message); } } function createEmptyTrackItemSelection() { let selection = null; const created = ppro.TrackItemSelection.createEmptySelection((value) => { selection = value; }); if (created !== true || !selection || typeof selection.addItem !== "function") { throw commandError("UXP_SELECTION_REJECTED", "Premiere could not create an empty timeline selection"); } return selection; } async function hostBoolean(value) { const resolved = value && typeof value.then === "function" ? await value : value; return resolved === true; } function activeSequenceGuid(sequence) { return sequence && sequence.guid != null ? String(sequence.guid) : ""; } function selectionSnapshotKeys(items) { return items.map((item) => JSON.stringify([ item.mediaType, item.trackIndex, item.clipIndex, item.projectItem && item.projectItem.id || "", item.startSeconds, item.endSeconds ])).sort(); } function selectionCoordinateKey(item) { return item.mediaType + ":" + item.trackIndex + ":" + item.clipIndex; } function sameStringArrays(left, right) { return left.length === right.length && left.every((value, index) => value === right[index]); } async function selectedItemsForEffect(context, mediaType) { const selected = await selectedTrackItems(context.sequence), classified = await classifySelection(context.sequence, selected.items); const invalid = classified.filter((value) => value.mediaType !== mediaType); if (invalid.length) throw commandError("UXP_SELECTION_TYPE_MISMATCH", "Every selected item must be a " + mediaType + " clip for this compound operation"); return classified; } async function addEffectToSelection(args) { const input = validateEffectAdd(args, true), context = await activeContext(true); const targets = await selectedItemsForEffect(context, input.mediaType), prepared = []; for (const target of targets) { const chain = await componentChain(target.item), before = chain.getComponentCount(); if (input.insertionIndex != null && input.insertionIndex > before) throw commandError("UXP_INVALID_ARGUMENT", "insertionIndex exceeds a selected clip's component count"); prepared.push({ target, chain, before, component: await createEffectComponent(input.mediaType, input.effectId, target.item) }); } let committed = false; context.project.lockedAccess(() => { committed = context.project.executeTransaction((compoundAction) => { for (const value of prepared) { const action = input.insertionIndex == null ? value.chain.createAppendComponentAction(value.component) : value.chain.createInsertComponentAction(value.component, input.insertionIndex); if (!action || compoundAction.addAction(action) === false) throw commandError("UXP_ACTION_REJECTED", "Premiere rejected a selected effect action"); } }, "Add effect to selected clips"); }); assertCommitted(committed, "selected effect addition"); const evidence = [], verified = await verifyChainDeltas(prepared, 1, evidence); return mutationResult(verified, { applied: prepared.length, mediaType: input.mediaType, effectId: input.effectId, insertionIndex: input.insertionIndex, evidence }, "selected_effect_chain_count_readback", "Add effect to selected clips"); } async function removeEffectFromSelection(args) { const input = validateEffectRemove(args, true), context = await activeContext(true); const targets = await selectedItemsForEffect(context, input.mediaType), prepared = []; for (const target of targets) { const chain = await componentChain(target.item), before = chain.getComponentCount(); if (input.componentIndex >= before) throw commandError("UXP_TARGET_NOT_FOUND", "componentIndex is out of range on a selected clip"); const component = chain.getComponentAtIndex(input.componentIndex); await assertExpectedComponent(component, input.expectedEffectId); prepared.push({ target, chain, before, component }); } let committed = false; context.project.lockedAccess(() => { committed = context.project.executeTransaction((compoundAction) => { for (const value of prepared) { const action = value.chain.createRemoveComponentAction(value.component); if (!action || compoundAction.addAction(action) === false) throw commandError("UXP_ACTION_REJECTED", "Premiere rejected a selected effect removal action"); } }, "Remove effect from selected clips"); }); assertCommitted(committed, "selected effect removal"); const evidence = [], verified = await verifyChainDeltas(prepared, -1, evidence); return mutationResult(verified, { removed: prepared.length, mediaType: input.mediaType, componentIndex: input.componentIndex, expectedEffectId: input.expectedEffectId, evidence }, "selected_effect_chain_count_readback", "Remove effect from selected clips"); } async function assertExpectedComponent(component, expectedEffectId) { const snapshot = await componentInfo(component, null); if (snapshot.matchName !== expectedEffectId && snapshot.displayName !== expectedEffectId) { throw commandError("UXP_STALE_EFFECT_CHAIN", "The component at componentIndex no longer matches expectedEffectId"); } } async function verifyChainDeltas(prepared, delta, evidence) { let verified = true; for (const value of prepared) { const afterCount = value.chain.getComponentCount(), expected = value.before + delta; if (afterCount !== expected) verified = false; evidence.push({ selectionIndex: value.target.selectionIndex, beforeCount: value.before, afterCount, expected }); } return verified; } async function detectScenes(args) { assertObject(args); assertOnlyKeys(args, ["mode", "operationId"]); const mode = enumValue(args.mode, "mode", ["applyCuts", "createMarkers", "createSubclips"]); const context = await activeContext(false), selected = await selectedTrackItems(context.sequence); const markerSnapshot = async () => { if (!ppro.Markers || typeof ppro.Markers.getMarkers !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere marker readback is required before scene-marker detection can run"); const ownerIds = new Set(), markerIds = new Set(); for (const trackItem of selected.items) { if (!trackItem || typeof trackItem.getProjectItem !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "Selected timeline items must expose project-item marker owners for scene-marker readback"); const owner = await trackItem.getProjectItem(), ownerId = await projectItemIdentifier(owner); if (!ownerId || ownerIds.has(ownerId)) continue; ownerIds.add(ownerId); const collection = await ppro.Markers.getMarkers(castProjectItem(owner)); if (!collection || typeof collection.getMarkers !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere did not expose a marker collection for a selected scene-edit item"); for (const marker of Array.from(await collection.getMarkers() || [])) { let markerId = ""; try { markerId = marker && marker.guid != null ? String(marker.guid) : ""; } catch (_) {} if (markerId) markerIds.add(ownerId + ":" + markerId); } } return { ownerIds: Array.from(ownerIds), markerIds: Array.from(markerIds) }; }; let markersBefore = null; if (mode === "createMarkers") { markersBefore = await markerSnapshot(); } const utils = ppro.SequenceUtils, names = { applyCuts: "SEQUENCE_OPERATION_APPLYCUT", createMarkers: "SEQUENCE_OPERATION_CREATEMARKER", createSubclips: "SEQUENCE_OPERATION_CREATESUBCLIP" }; const operation = utils && utils[names[mode]]; if (typeof operation !== "string" || !operation) throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere scene-edit operation constants are unavailable"); const detected = await utils.performSceneEditDetectionOnSelection(operation, selected.selection); if (!detected) throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not confirm scene-edit detection"); if (mode === "createMarkers") { const markersAfter = await markerSnapshot(), addedMarkerIds = markersAfter.markerIds.filter((id) => markersBefore.markerIds.indexOf(id) < 0); if (!addedMarkerIds.length) throw commandError("UXP_VERIFICATION_FAILED", "Premiere reported scene-marker detection but did not add observable selected-item markers"); return { detected: true, verified: true, mode, selectedItemCount: selected.items.length, markerOwnerCount: markersAfter.ownerIds.length, addedMarkerCount: addedMarkerIds.length, outcome: "verified", verificationBoundary: "selected_project_item_marker_guid_readback", operation: operationSemantics({ mutatesProject: true, verificationStatus: "verified", verificationBoundary: "selected_project_item_marker_guid_readback", verificationEvidence: [{ type: "selected_project_item_marker_guid_delta", addedMarkerCount: addedMarkerIds.length }], undoSupported: false, cancellationSupported: true }) }; } return { detected: true, outcome: "committed_unverified", mode, selectedItemCount: selected.items.length, verificationBoundary: "sequence_utils_host_return", operation: operationSemantics({ mutatesProject: true, verificationStatus: "not_verified", verificationBoundary: "sequence_utils_host_return", verificationEvidence: [{ type: "host_return", value: true }], undoSupported: false, cancellationSupported: true }) }; } async function proxySnapshot(clip) { const projectItem = castProjectItem(clip); const result = { projectItemId: await projectItemIdentifier(clip), name: String(clip.name || ""), canProxy: null, hasProxy: null, proxyPath: "", offline: null, mediaPath: "" }; if (typeof clip.canProxy === "function") result.canProxy = !!await clip.canProxy(); if (typeof clip.hasProxy === "function") result.hasProxy = !!await clip.hasProxy(); if (typeof clip.getProxyPath === "function") result.proxyPath = String(await clip.getProxyPath() || ""); if (typeof clip.isOffline === "function") result.offline = !!await clip.isOffline(); if (projectItem && typeof projectItem.getMediaFilePath === "function") result.mediaPath = String(await projectItem.getMediaFilePath() || ""); return result; } async function inspectProxy(args) { const context = await clipTarget(args, ["projectItemId", "projectItemName"]); return proxySnapshot(context.clip); } async function attachProxy(args) { assertObject(args); assertOnlyKeys(args, ["projectItemId", "projectItemName", "mediaPath", "isHiRes", "makeAlternateLinkInTeamProjects", "replaceExistingProxy", "confirmNonUndoable", "operationId"]); const target = validateProjectItemTarget(args); requireConfirmation(args.confirmNonUndoable, "Attaching proxy or high-resolution media is not undoable"); const mediaPath = await allowedPath(args.mediaPath, "proxy mediaPath", "file"); const isHiRes = optionalBoolean(args.isHiRes, false, "isHiRes"); const alternate = optionalBoolean(args.makeAlternateLinkInTeamProjects, false, "makeAlternateLinkInTeamProjects"); const replaceExistingProxy = optionalBoolean(args.replaceExistingProxy, false, "replaceExistingProxy"); const project = await activeProject(false), clip = await resolveClipProjectItem(project, target), before = await proxySnapshot(clip); if (typeof clip.canProxy !== "function" || !await clip.canProxy()) throw commandError("UXP_TARGET_UNSUPPORTED", "The resolved clip cannot accept proxy media"); if (!isHiRes && before.hasProxy && pathEqual(before.proxyPath, mediaPath)) { return { attached: false, unchanged: true, outcome: "verified", before, after: before, mediaPath, isHiRes }; } if (!isHiRes && before.hasProxy && !replaceExistingProxy) { throw commandError("UXP_PROXY_ALREADY_ATTACHED", "A different proxy is already attached; inspect it and pass replaceExistingProxy=true to replace it"); } const attached = await clip.attachProxy(mediaPath, isHiRes, alternate); if (!attached) throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not confirm proxy attachment"); const after = await proxySnapshot(clip); const verified = isHiRes ? false : after.hasProxy === true && pathEqual(after.proxyPath, mediaPath); return { attached: true, outcome: verified ? "verified" : "committed_unverified", before, after, mediaPath, isHiRes, replaceExistingProxy, verificationBoundary: verified ? "proxy_path_readback" : "attach_proxy_host_return", operation: operationSemantics({ mutatesProject: true, verificationStatus: verified ? "verified" : "not_verified", verificationBoundary: verified ? "proxy_path_readback" : "attach_proxy_host_return", verificationEvidence: verified ? [{ type: "proxy_path", pathMatched: true }] : [{ type: "host_return", value: true }], undoSupported: false, cancellationSupported: true }) }; } async function ingestSnapshot(project) { const settings = await ppro.ProjectSettings.getIngestSettings(project); if (!settings || typeof settings.getIsIngestEnabled !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere did not return ingest settings"); return { settings, enabled: !!await settings.getIsIngestEnabled() }; } async function getIngest(args) { assertObject(args); assertOnlyKeys(args, []); const project = await activeProject(false), snapshot = await ingestSnapshot(project); return { enabled: snapshot.enabled }; } async function configureIngest(args) { assertObject(args); assertOnlyKeys(args, ["enabled", "operationId"]); const enabled = requiredBoolean(args.enabled, "enabled"), project = await activeProject(true); const before = await ingestSnapshot(project); if (before.enabled === enabled) return { configured: false, unchanged: true, outcome: "verified", enabled }; if (typeof before.settings.setIngestEnabled !== "function" || !await before.settings.setIngestEnabled(enabled)) { throw commandError("UXP_ACTION_REJECTED", "Premiere rejected the ingest setting value"); } let committed = false; project.lockedAccess(() => { const action = ppro.ProjectSettings.createSetIngestSettingsAction(project, before.settings); committed = project.executeTransaction((compoundAction) => { if (!action || compoundAction.addAction(action) === false) throw commandError("UXP_ACTION_REJECTED", "Premiere rejected the ingest settings action"); }, "Configure ingest"); }); assertCommitted(committed, "ingest settings update"); const after = await ingestSnapshot(project), verified = after.enabled === enabled; return mutationResult(verified, { configured: true, before: before.enabled, enabled: after.enabled }, "ingest_settings_readback", "Configure ingest"); } async function relinkMedia(args) { assertObject(args); assertOnlyKeys(args, ["projectItemId", "projectItemName", "newPath", "expectedCurrentPath", "overrideCompatibilityCheck", "requireOffline", "confirmNonUndoable", "operationId"]); const target = validateProjectItemTarget(args); requireConfirmation(args.confirmNonUndoable, "Changing a clip's media path is not undoable"); const newPath = await allowedPath(args.newPath, "newPath", "file"); const expectedCurrentPath = args.expectedCurrentPath == null ? null : boundedString(args.expectedCurrentPath, "expectedCurrentPath", 4096); const overrideCompatibilityCheck = optionalBoolean(args.overrideCompatibilityCheck, false, "overrideCompatibilityCheck"); const requireOffline = optionalBoolean(args.requireOffline, true, "requireOffline"); const project = await activeProject(false), clip = await resolveClipProjectItem(project, target), projectItem = castProjectItem(clip); if (typeof clip.canChangeMediaPath !== "function" || !await clip.canChangeMediaPath()) throw commandError("UXP_TARGET_UNSUPPORTED", "Premiere reports that this clip's media path cannot be changed"); const before = await proxySnapshot(clip); if (expectedCurrentPath != null && !pathEqual(before.mediaPath, expectedCurrentPath)) { throw commandError("UXP_STALE_MEDIA_PATH", "The clip's current media path no longer matches expectedCurrentPath"); } if (requireOffline && before.offline !== true) throw commandError("UXP_MEDIA_NOT_OFFLINE", "Safe relink defaults to offline clips; set requireOffline=false only after inspecting the target"); if (pathEqual(before.mediaPath, newPath) && before.offline === false) { return { relinked: false, unchanged: true, outcome: "verified", before, after: before, newPath }; } const changed = await clip.changeMediaFilePath(newPath, overrideCompatibilityCheck); if (!changed) throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not confirm the media relink"); if (typeof clip.refreshMedia === "function") await clip.refreshMedia(); const after = await proxySnapshot(clip); if (!after.mediaPath && projectItem && typeof projectItem.getMediaFilePath === "function") after.mediaPath = String(await projectItem.getMediaFilePath() || ""); const verified = pathEqual(after.mediaPath, newPath) && after.offline === false; return { relinked: true, outcome: verified ? "verified" : "committed_unverified", before, after, newPath, overrideCompatibilityCheck, verificationBoundary: verified ? "media_path_and_online_readback" : "change_media_file_path_host_return", operation: operationSemantics({ mutatesProject: true, verificationStatus: verified ? "verified" : "not_verified", verificationBoundary: verified ? "media_path_and_online_readback" : "change_media_file_path_host_return", verificationEvidence: [{ type: "media_path", pathMatched: pathEqual(after.mediaPath, newPath), online: after.offline === false }], undoSupported: false, cancellationSupported: true }) }; } async function metadataSnapshot(clip) { const projectItem = castProjectItem(clip); const projectMetadata = String(await ppro.Metadata.getProjectMetadata(projectItem) || ""); const xmpMetadata = String(await ppro.Metadata.getXMPMetadata(projectItem) || ""); const result = { projectItemId: await projectItemIdentifier(projectItem), name: String(clip.name || ""), projectMetadata, xmpMetadata }; if (projectMetadata.length > MAX_METADATA_CHARS || xmpMetadata.length > MAX_METADATA_CHARS || utf8ByteLength(JSON.stringify(result)) > MAX_METADATA_RESULT_BYTES) { throw commandError("UXP_RESULT_TOO_LARGE", "Metadata exceeds the bridge's bounded result size"); } return Protocol && typeof Protocol.assertResultSize === "function" ? Protocol.assertResultSize(result) : result; } async function getMetadata(args) { const context = await clipTarget(args, ["projectItemId", "projectItemName"]); return metadataSnapshot(context.clip); } function boundedMetadataResult(value, field, result) { const metadata = boundedStringAllowEmpty(value, field, MAX_METADATA_CHARS); if (utf8ByteLength(JSON.stringify(result)) > MAX_METADATA_RESULT_BYTES) { throw commandError("UXP_RESULT_TOO_LARGE", field + " exceeds the bridge's bounded result size"); } return Protocol && typeof Protocol.assertResultSize === "function" ? Protocol.assertResultSize(result) : result; } async function getProjectColumnsMetadata(args) { const context = await clipTarget(args, ["projectItemId", "projectItemName"]); const projectItem = castProjectItem(context.clip); const projectColumnsMetadata = await ppro.Metadata.getProjectColumnsMetadata(projectItem); const result = { projectItemId: await projectItemIdentifier(projectItem), name: String(context.clip.name || ""), projectColumnsMetadata }; return boundedMetadataResult(result.projectColumnsMetadata, "projectColumnsMetadata", result); } async function getProjectPanelMetadata(args) { assertObject(args); assertOnlyKeys(args, []); const project = await activeProject(false); const projectPanelMetadata = await ppro.Metadata.getProjectPanelMetadata(); const result = { projectGuid: String(project.guid || ""), projectName: String(project.name || ""), projectPanelMetadata }; if (!result.projectGuid) throw commandError("UXP_INVALID_HOST_STATE", "Premiere did not return a project GUID for Project-panel metadata"); return boundedMetadataResult(result.projectPanelMetadata, "projectPanelMetadata", result); } async function projectPanelMetadataSnapshot(maximumBytes) { const project = await activeProject(false); const projectGuid = String(project.guid || ""); if (!projectGuid) throw commandError("UXP_INVALID_HOST_STATE", "Premiere did not return a project GUID for Project-panel metadata"); const projectPanelMetadata = boundedUtf8StringAllowEmpty( await ppro.Metadata.getProjectPanelMetadata(), "projectPanelMetadata", maximumBytes ); return { project, projectGuid, projectName: String(project.name || ""), projectPanelMetadata }; } async function updateProjectPanelMetadata(args) { assertObject(args); assertOnlyKeys(args, ["expectedProjectGuid", "expectedProjectPanelMetadata", "projectPanelMetadata", "confirmUpdate", "operationId"]); const input = { expectedProjectGuid: boundedString(args.expectedProjectGuid, "expectedProjectGuid", 512), expectedProjectPanelMetadata: boundedUtf8StringAllowEmpty(args.expectedProjectPanelMetadata, "expectedProjectPanelMetadata", MAX_PROJECT_PANEL_METADATA_UPDATE_BYTES), projectPanelMetadata: boundedUtf8StringAllowEmpty(args.projectPanelMetadata, "projectPanelMetadata", MAX_PROJECT_PANEL_METADATA_UPDATE_BYTES), operationId: requiredOperationId(args.operationId, "operationId") }; if (args.confirmUpdate !== true) { throw commandError("UXP_CONFIRMATION_REQUIRED", "Replacing Project-panel metadata is not undoable; pass confirmUpdate=true after review"); } return withProjectPanelMetadataMutationLock(input.expectedProjectGuid, async () => { // This queue serializes this bridge's competing requests. Premiere does // not expose an atomic compare-and-set for this direct setter, so the // snapshot is deliberately the final asynchronous preflight before the // setter is started; human UI or another extension can still race it. const before = await projectPanelMetadataSnapshot(MAX_PROJECT_PANEL_METADATA_UPDATE_BYTES); if (before.projectGuid !== input.expectedProjectGuid) { throw commandError("UXP_STALE_PROJECT_PANEL_METADATA", "The active project changed before Project-panel metadata was updated; inspect and retry"); } if (before.projectPanelMetadata !== input.expectedProjectPanelMetadata) { throw commandError("UXP_STALE_PROJECT_PANEL_METADATA", "Project-panel metadata changed before it was updated; inspect and retry"); } if (before.projectPanelMetadata === input.projectPanelMetadata) { return projectPanelMetadataUpdateResult(true, { updated: false, unchanged: true, projectGuid: before.projectGuid, projectName: before.projectName, projectPanelMetadata: before.projectPanelMetadata }, "project_panel_metadata_exact_readback"); } let request; before.project.lockedAccess(() => { request = ppro.Metadata.setProjectPanelMetadata(input.projectPanelMetadata); }); if (await request !== true) throw commandError("UXP_ACTION_REJECTED", "Premiere did not accept Project-panel metadata replacement"); const after = await projectPanelMetadataSnapshot(MAX_PROJECT_PANEL_METADATA_UPDATE_BYTES); const verified = after.projectGuid === before.projectGuid && after.projectPanelMetadata === input.projectPanelMetadata; return projectPanelMetadataUpdateResult(verified, { updated: true, projectGuid: before.projectGuid, projectName: after.projectName, projectPanelMetadata: after.projectPanelMetadata, requestedProjectPanelMetadata: input.projectPanelMetadata, readbackProjectGuid: after.projectGuid }, verified ? "project_panel_metadata_exact_readback" : "project_panel_metadata_active_project_readback"); }); } function withProjectPanelMetadataMutationLock(projectGuid, operation) { const previous = projectPanelMetadataMutationTails.get(projectGuid) || Promise.resolve(); let release; const gate = new Promise((resolve) => { release = resolve; }); const tail = previous.catch(() => undefined).then(() => gate); projectPanelMetadataMutationTails.set(projectGuid, tail); return previous.catch(() => undefined).then(operation).finally(() => { release(); if (projectPanelMetadataMutationTails.get(projectGuid) === tail) projectPanelMetadataMutationTails.delete(projectGuid); }); } function projectPanelMetadataUpdateResult(verified, values, boundary) { return { ...values, outcome: verified ? "verified" : "committed_unverified", verified, verificationBoundary: boundary, operation: operationSemantics({ mutatesProject: true, verificationStatus: verified ? "verified" : "not_verified", verificationBoundary: boundary, verificationEvidence: [{ type: boundary, verified }], undoSupported: false, transactionActionGroup: false, cancellationSupported: false }) }; } async function inspectProjectMetadataSchema(args) { assertObject(args); assertOnlyKeys(args, []); const snapshot = await projectPanelMetadataSnapshot(MAX_PROJECT_PANEL_METADATA_UPDATE_BYTES); return { projectGuid: snapshot.projectGuid, projectName: snapshot.projectName, projectPanelMetadata: snapshot.projectPanelMetadata, verificationBoundary: "bounded_project_panel_metadata_readback" }; } async function createProjectMetadataField(args) { assertObject(args); assertOnlyKeys(args, ["expectedProjectGuid", "expectedProjectPanelMetadata", "fieldName", "fieldLabel", "fieldType", "confirmCreate", "operationId"]); const input = { expectedProjectGuid: boundedString(args.expectedProjectGuid, "expectedProjectGuid", 512), expectedProjectPanelMetadata: boundedUtf8StringAllowEmpty(args.expectedProjectPanelMetadata, "expectedProjectPanelMetadata", MAX_PROJECT_PANEL_METADATA_UPDATE_BYTES), fieldName: metadataSchemaFieldName(args.fieldName), fieldLabel: boundedString(args.fieldLabel, "fieldLabel", 255), fieldType: enumValue(args.fieldType, "fieldType", ["integer", "real", "text", "boolean"]), operationId: requiredOperationId(args.operationId, "operationId") }; if (args.confirmCreate !== true) { throw commandError("UXP_CONFIRMATION_REQUIRED", "Creating a Project metadata schema field is non-undoable; pass confirmCreate=true after review"); } // Resolve every synchronous host value before the queued stale snapshot so // no awaited conversion can open a gap between validation and the direct // schema call under lockedAccess(). const metadataType = projectMetadataType(input.fieldType); return withProjectPanelMetadataMutationLock(input.expectedProjectGuid, async () => { // This shared queue also excludes this bridge's direct Project-panel XML // replacement requests. Adobe provides no atomic compare-and-set or // schema-field getter, so UI and other-extension races remain outside // this protocol's proof boundary. const before = await projectPanelMetadataSnapshot(MAX_PROJECT_PANEL_METADATA_UPDATE_BYTES); if (before.projectGuid !== input.expectedProjectGuid) { throw commandError("UXP_STALE_PROJECT_METADATA_SCHEMA", "The active project changed before the metadata schema field was created; inspect and retry"); } if (before.projectPanelMetadata !== input.expectedProjectPanelMetadata) { throw commandError("UXP_STALE_PROJECT_METADATA_SCHEMA", "Project-panel metadata changed before the metadata schema field was created; inspect and retry"); } let request; before.project.lockedAccess(() => { request = ppro.Metadata.addPropertyToProjectMetadataSchema(input.fieldName, input.fieldLabel, metadataType); }); if (await request !== true) throw commandError("UXP_ACTION_REJECTED", "Premiere did not accept Project metadata schema field creation"); const after = await projectPanelMetadataSnapshot(MAX_PROJECT_PANEL_METADATA_UPDATE_BYTES); const activeProjectRetained = after.projectGuid === before.projectGuid; const panelMetadataChanged = activeProjectRetained && after.projectPanelMetadata !== before.projectPanelMetadata; return { creationRequested: true, hostAccepted: true, projectGuid: before.projectGuid, projectName: after.projectName, field: { name: input.fieldName, label: input.fieldLabel, type: input.fieldType }, readbackProjectGuid: after.projectGuid, panelMetadataChanged, outcome: "committed_unverified", verified: false, verificationBoundary: panelMetadataChanged ? "project_panel_metadata_change_readback" : "metadata_schema_add_host_return", operation: operationSemantics({ mutatesProject: true, verificationStatus: "not_verified", verificationBoundary: panelMetadataChanged ? "project_panel_metadata_change_readback" : "metadata_schema_add_host_return", verificationEvidence: [{ type: "metadata_schema_host_return", value: true }, { type: "project_panel_metadata_changed", value: panelMetadataChanged }], undoSupported: false, transactionActionGroup: false, cancellationSupported: false }) }; }); } function metadataSchemaFieldName(value) { if (typeof value !== "string" || !/^[A-Za-z][A-Za-z0-9_.-]{0,127}$/.test(value)) { throw commandError("UXP_INVALID_ARGUMENT", "fieldName must start with a letter and contain at most 128 letters, digits, periods, underscores, or hyphens"); } return value; } function projectMetadataType(fieldType) { const typeNames = { integer: "METADATA_TYPE_INTEGER", real: "METADATA_TYPE_REAL", text: "METADATA_TYPE_TEXT", boolean: "METADATA_TYPE_BOOLEAN" }; const value = ppro.Metadata && ppro.Metadata[typeNames[fieldType]]; if (value == null) throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere does not expose the " + fieldType + " Project metadata field type"); return value; } async function updateMetadata(args) { assertObject(args); assertOnlyKeys(args, ["projectItemId", "projectItemName", "projectMetadata", "xmpMetadata", "updatedFields", "operationId"]); const target = validateProjectItemTarget(args), hasProject = args.projectMetadata != null, hasXmp = args.xmpMetadata != null; if (!hasProject && !hasXmp) throw commandError("UXP_INVALID_ARGUMENT", "At least one of projectMetadata or xmpMetadata is required"); const projectMetadata = hasProject ? boundedStringAllowEmpty(args.projectMetadata, "projectMetadata", MAX_METADATA_CHARS) : null; const xmpMetadata = hasXmp ? boundedStringAllowEmpty(args.xmpMetadata, "xmpMetadata", MAX_METADATA_CHARS) : null; const updatedFields = validateUpdatedFields(args.updatedFields, hasProject); const project = await activeProject(true), clip = await resolveClipProjectItem(project, target), projectItem = castProjectItem(clip); const before = await metadataSnapshot(clip); if ((!hasProject || before.projectMetadata === projectMetadata) && (!hasXmp || before.xmpMetadata === xmpMetadata)) { return { updated: false, unchanged: true, outcome: "verified", metadata: before }; } let committed = false; project.lockedAccess(() => { committed = project.executeTransaction((compoundAction) => { if (hasProject && before.projectMetadata !== projectMetadata) { const projectAction = ppro.Metadata.createSetProjectMetadataAction(projectItem, projectMetadata, updatedFields); if (!projectAction || compoundAction.addAction(projectAction) === false) throw commandError("UXP_ACTION_REJECTED", "Premiere rejected the project metadata action"); } if (hasXmp && before.xmpMetadata !== xmpMetadata) { const xmpAction = ppro.Metadata.createSetXMPMetadataAction(projectItem, xmpMetadata); if (!xmpAction || compoundAction.addAction(xmpAction) === false) throw commandError("UXP_ACTION_REJECTED", "Premiere rejected the XMP metadata action"); } }, "Update clip metadata"); }); assertCommitted(committed, "metadata update"); const after = await metadataSnapshot(clip); const verified = (!hasProject || after.projectMetadata === projectMetadata) && (!hasXmp || after.xmpMetadata === xmpMetadata); return mutationResult(verified, { updated: true, updatedFields, metadata: after }, "metadata_readback", "Update clip metadata"); } async function footageSnapshot(clip) { if (typeof clip.getFootageInterpretation !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "Footage interpretation APIs are unavailable for this clip"); const interpretation = await clip.getFootageInterpretation(); if (!interpretation) throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere did not return footage interpretation data"); const values = {}; const getters = { frameRate: "getFrameRate", pixelAspectRatio: "getPixelAspectRatio", fieldType: "getFieldType", removePullDown: "getRemovePullDown", alphaUsage: "getAlphaUsage", ignoreAlpha: "getIgnoreAlpha", invertAlpha: "getInvertAlpha", vrConform: "getVrConform", vrLayout: "getVrLayout", vrHorzView: "getVrHorzView", vrVertView: "getVrVertView", footageInputLutId: "getInputLUTID" }; for (const key of Object.keys(getters)) { const method = getters[key]; values[key] = typeof interpretation[method] === "function" ? interpretation[method]() : null; } values.inputLutId = typeof clip.getInputLUTID === "function" ? String(await clip.getInputLUTID() || "") : null; values.embeddedLutId = typeof clip.getEmbeddedLUTID === "function" ? String(await clip.getEmbeddedLUTID() || "") : null; return { interpretation, values }; } async function colorPreflight(args) { const context = await clipTarget(args, ["projectItemId", "projectItemName"]); if (typeof context.project.getColorSettings !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "Project color settings are unavailable"); const settings = await context.project.getColorSettings(); if (!settings) throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere did not return project color settings"); const footage = await footageSnapshot(context.clip); return { project: { graphicsWhiteLuminance: await settings.getGraphicsWhiteLuminance(), supportedGraphicsWhiteLuminances: Array.from(await settings.getSupportedGraphicsWhiteLuminances() || []) }, clip: { projectItemId: await projectItemIdentifier(context.clip), name: String(context.clip.name || ""), ...footage.values } }; } async function inspectEnvironment(args) { assertObject(args); assertOnlyKeys(args, []); const project = await activeProject(false); if (!ppro.Utils || typeof ppro.Utils.isAEInstalled !== "function") { throw commandError("UXP_COMMAND_UNAVAILABLE", "After Effects installation detection is unavailable"); } if (typeof project.getColorSettings !== "function") { throw commandError("UXP_COMMAND_UNAVAILABLE", "Project color settings are unavailable"); } const settings = await project.getColorSettings(); if (!settings || typeof settings.getGraphicsWhiteLuminance !== "function" || typeof settings.getSupportedGraphicsWhiteLuminances !== "function") { throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere did not return complete project color settings"); } return { afterEffectsInstalled: !!await ppro.Utils.isAEInstalled(), projectColor: { graphicsWhiteLuminance: await settings.getGraphicsWhiteLuminance(), supportedGraphicsWhiteLuminances: Array.from(await settings.getSupportedGraphicsWhiteLuminances() || []) } }; } async function conformFootage(args) { const input = validateConformanceArgs(args), project = await activeProject(true); const clip = await resolveClipProjectItem(project, input), before = await footageSnapshot(clip); const setters = { frameRate: "setFrameRate", pixelAspectRatio: "setPixelAspectRatio", fieldType: "setFieldType", removePullDown: "setRemovePullDown", alphaUsage: "setAlphaUsage", ignoreAlpha: "setIgnoreAlpha", invertAlpha: "setInvertAlpha", vrConform: "setVrConform", vrLayout: "setVrLayout", vrHorzView: "setVrHorzView", vrVertView: "setVrVertView" }; const interpretationKeys = Object.keys(setters).filter((key) => input[key] != null); for (const key of interpretationKeys) { const method = setters[key]; if (typeof before.interpretation[method] !== "function" || before.interpretation[method](input[key]) !== true) { throw commandError("UXP_ACTION_REJECTED", "Premiere rejected footage interpretation field " + key); } } let committed = false; project.lockedAccess(() => { committed = project.executeTransaction((compoundAction) => { if (interpretationKeys.length) { const interpretationAction = clip.createSetFootageInterpretationAction(before.interpretation); if (!interpretationAction || compoundAction.addAction(interpretationAction) === false) throw commandError("UXP_ACTION_REJECTED", "Premiere rejected the footage interpretation action"); } if (input.inputLutId != null) { const lutAction = clip.createSetInputLUTIDAction(input.inputLutId); if (!lutAction || compoundAction.addAction(lutAction) === false) throw commandError("UXP_ACTION_REJECTED", "Premiere rejected the input LUT action"); } }, "Conform source footage"); }); assertCommitted(committed, "footage conformance update"); const after = await footageSnapshot(clip), requested = { ...input }; delete requested.projectItemId; delete requested.projectItemName; delete requested.operationId; const verified = Object.keys(requested).every((key) => valuesEqual(after.values[key], requested[key])); return mutationResult(verified, { conformed: true, projectItemId: await projectItemIdentifier(clip), requested, before: before.values, after: after.values }, "footage_interpretation_readback", "Conform source footage"); } async function sourceMonitorState(args) { assertObject(args); assertOnlyKeys(args, []); const source = ppro.SourceMonitor, position = await source.getPosition(); let projectItem = null; try { const item = await source.getProjectItem(); if (item) projectItem = { id: await projectItemIdentifier(item), name: String(item.name || "") }; } catch (_) {} return { open: !!projectItem, positionSeconds: tickSeconds(position), projectItem }; } async function openSourceMonitor(args) { assertObject(args); assertOnlyKeys(args, ["projectItemId", "projectItemName", "filePath", "operationId"]); const hasFile = args.filePath != null; if (hasFile && (args.projectItemId != null || args.projectItemName != null)) throw commandError("UXP_INVALID_ARGUMENT", "filePath cannot be combined with a project-item selector"); if (hasFile) { const filePath = await allowedPath(args.filePath, "Source Monitor filePath", "file"); const opened = await ppro.SourceMonitor.openFilePath(filePath); if (!opened) throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not confirm opening the file in Source Monitor"); return { opened: true, source: "file", filePath, outcome: "committed_unverified", verificationBoundary: "source_monitor_open_file_host_return", operation: operationSemantics({ mutatesProject: false, verificationStatus: "not_verified", verificationBoundary: "source_monitor_open_file_host_return", verificationEvidence: [{ type: "host_return", value: true }] }) }; } const target = validateProjectItemTarget(args), project = await activeProject(false); const clip = await resolveClipProjectItem(project, target), projectItem = castProjectItem(clip), expectedId = await projectItemIdentifier(projectItem); const opened = await ppro.SourceMonitor.openProjectItem(projectItem); if (!opened) throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not confirm opening the project item in Source Monitor"); const readback = await ppro.SourceMonitor.getProjectItem(), readbackId = await projectItemIdentifier(readback); const verified = !!expectedId && readbackId === expectedId; return { opened: true, source: "projectItem", projectItemId: expectedId, name: String(clip.name || ""), outcome: verified ? "verified" : "committed_unverified", verificationBoundary: "source_monitor_project_item_readback", operation: operationSemantics({ mutatesProject: false, verificationStatus: verified ? "verified" : "not_verified", verificationBoundary: "source_monitor_project_item_readback", verificationEvidence: [{ type: "project_item", expectedId, readbackId }] }) }; } async function playSourceMonitor(args) { assertObject(args); assertOnlyKeys(args, ["speed", "operationId"]); const speed = args.speed == null ? 1 : finiteNumber(args.speed, "speed", -16, 16); const played = await ppro.SourceMonitor.play(speed); if (!played) throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not confirm Source Monitor playback"); return { playing: true, speed, outcome: "committed_unverified", verificationBoundary: "source_monitor_play_host_return" }; } async function closeSourceMonitor(args) { assertObject(args); assertOnlyKeys(args, ["all", "operationId"]); const all = optionalBoolean(args.all, false, "all"); const closed = all ? await ppro.SourceMonitor.closeAllClips() : await ppro.SourceMonitor.closeClip(); if (!closed) throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not confirm closing Source Monitor media"); let state = null; try { state = await sourceMonitorState({}); } catch (_) {} const verified = !!state && state.open === false; return { closed: true, all, outcome: verified ? "verified" : "committed_unverified", state, verificationBoundary: verified ? "source_monitor_state_readback" : "source_monitor_close_host_return" }; } function scratchTypeTable() { const constants = ppro.Constants && ppro.Constants.ScratchDiskFolderType || {}; return { capture: constants.CAPTURE, audioPreview: constants.AUDIO_PREVIEW, videoPreview: constants.VIDEO_PREVIEW, autoSave: constants.AUTO_SAVE, ccLibraries: constants.CCL_LIBRARIES, capsuleMedia: constants.CAPSULE_MEDIA }; } function scratchSnapshot(settings) { const result = {}, table = scratchTypeTable(); for (const key of Object.keys(table)) { if (table[key] == null) continue; try { result[key] = settings.getScratchDiskPath(table[key]); } catch (_) { result[key] = null; } } return result; } async function storagePreflight(args) { assertObject(args); assertOnlyKeys(args, []); const project = await activeProject(false), scratch = await ppro.ProjectSettings.getScratchDiskSettings(project); const ingest = await ingestSnapshot(project); let production = { apiAvailable: false, active: false, scratchDisks: null }; if (ppro.PRProduction && typeof ppro.PRProduction.getActiveProduction === "function") { production.apiAvailable = true; try { const active = ppro.PRProduction.getActiveProduction(); if (active) production = { apiAvailable: true, active: true, scratchDisks: scratchSnapshot(await active.getScratchDiskSettings()) }; } catch (_) {} } return { project: { scratchDisks: scratchSnapshot(scratch), ingestEnabled: ingest.enabled }, production }; } async function configureScratch(args) { assertObject(args); assertOnlyKeys(args, ["folderTypes", "destination", "operationId"]); const table = scratchTypeTable(), folderTypes = boundedEnumArray(args.folderTypes, "folderTypes", Object.keys(table), 6); const destination = enumValue(args.destination, "destination", ["sameAsProject", "myDocuments"]); const destinations = ppro.Constants && ppro.Constants.ScratchDiskFolder || {}; const destinationValue = destination === "sameAsProject" ? destinations.SAME_AS_PROJECT : destinations.MY_DOCUMENTS; if (destinationValue == null) throw commandError("UXP_COMMAND_UNAVAILABLE", "Scratch disk destination constants are unavailable"); const project = await activeProject(true), settings = await ppro.ProjectSettings.getScratchDiskSettings(project); const before = scratchSnapshot(settings); for (const key of folderTypes) { if (table[key] == null || settings.setScratchDiskPath(table[key], destinationValue) !== true) { throw commandError("UXP_ACTION_REJECTED", "Premiere rejected scratch disk folder type " + key); } } let committed = false; project.lockedAccess(() => { const action = ppro.ProjectSettings.createSetScratchDiskSettingsAction(project, settings); committed = project.executeTransaction((compoundAction) => { if (!action || compoundAction.addAction(action) === false) throw commandError("UXP_ACTION_REJECTED", "Premiere rejected the scratch disk settings action"); }, "Configure scratch disks"); }); assertCommitted(committed, "scratch disk update"); const afterSettings = await ppro.ProjectSettings.getScratchDiskSettings(project), after = scratchSnapshot(afterSettings); return { configured: true, outcome: "committed_unverified", folderTypes, destination, before, after, verificationBoundary: "scratch_disk_settings_readback", operation: operationSemantics({ mutatesProject: true, verificationStatus: "not_verified", verificationBoundary: "scratch_disk_settings_readback", verificationEvidence: [{ type: "scratch_disk_snapshot", folderTypes, destination }], undoSupported: true, undoLabel: "Configure scratch disks", transactionActionGroup: true, cancellationSupported: true }) }; } async function workspaceStatus(args) { assertObject(args); assertOnlyKeys(args, []); if (!workspace || typeof workspace.status !== "function") return { configured: false, accessMode: "unavailable", rootName: null, persistent: false, pathDisclosure: "redacted" }; return workspace.status(); } async function allowedPath(value, label, kind) { const path = boundedString(value, label, 4096); return workspace && typeof workspace.assertPathAllowed === "function" ? await workspace.assertPathAllowed(path, { label, kind }) : path; } function operationSemantics(options) { return Protocol && typeof Protocol.operationSemantics === "function" ? Protocol.operationSemantics(options) : undefined; } function mutationResult(verified, values, boundary, undoLabel) { return { ...values, outcome: verified ? "verified" : "committed_unverified", verified, verificationBoundary: boundary, operation: operationSemantics({ mutatesProject: true, verificationStatus: verified ? "verified" : "not_verified", verificationBoundary: boundary, verificationEvidence: [{ type: boundary, verified }], undoSupported: true, undoLabel, transactionActionGroup: true, cancellationSupported: true }) }; } function assertCommitted(committed, operation) { if (!committed) throw commandError("UXP_TRANSACTION_FAILED", "Premiere did not commit the " + operation + " transaction"); } function canInspectProject() { return !!(ppro.Project && typeof ppro.Project.getActiveProject === "function"); } function canUseClipItems() { return canInspectProject() && !!(ppro.ClipProjectItem && typeof ppro.ClipProjectItem.cast === "function"); } function canUseVideoEffects() { return !!(ppro.VideoFilterFactory && typeof ppro.VideoFilterFactory.createComponent === "function" && typeof ppro.VideoFilterFactory.getMatchNames === "function"); } function canUseAudioEffects() { return !!(ppro.AudioFilterFactory && typeof ppro.AudioFilterFactory.createComponentByDisplayName === "function" && typeof ppro.AudioFilterFactory.getDisplayNames === "function"); } function canUseEffects() { return canInspectProject() && (canUseVideoEffects() || canUseAudioEffects()); } function canInspectTrackItemIdentity() { return canInspectProject() && !!(ppro.Constants && ppro.Constants.TrackItemType); } function canUseSelection() { return canInspectProject() && !!(ppro.Constants && ppro.Constants.TrackItemType); } async function canManageSelection() { if (!canUseSelection() || !ppro.TrackItemSelection || typeof ppro.TrackItemSelection.createEmptySelection !== "function") return false; try { const project = await ppro.Project.getActiveProject(); if (!project) return true; const sequence = await project.getActiveSequence(); return !sequence || typeof sequence.getSelection === "function" && typeof sequence.setSelection === "function" && typeof sequence.clearSelection === "function"; } catch (_) { return false; } } function canUseEffectsSelection() { return canUseEffects() && canUseSelection(); } function canDetectScenes() { return canUseSelection() && !!(ppro.SequenceUtils && typeof ppro.SequenceUtils.performSceneEditDetectionOnSelection === "function"); } function canAttachProxy() { return canUseClipItems(); } function canRelink() { return canUseClipItems(); } function canUseProjectSettings() { return canInspectProject() && !!(ppro.ProjectSettings && typeof ppro.ProjectSettings.getScratchDiskSettings === "function"); } function canUseIngest() { return canInspectProject() && !!(ppro.ProjectSettings && typeof ppro.ProjectSettings.getIngestSettings === "function" && typeof ppro.ProjectSettings.createSetIngestSettingsAction === "function"); } function canUseMetadata() { return canUseClipItems() && !!(ppro.Metadata && typeof ppro.Metadata.getProjectMetadata === "function" && typeof ppro.Metadata.getXMPMetadata === "function" && typeof ppro.Metadata.createSetProjectMetadataAction === "function" && typeof ppro.Metadata.createSetXMPMetadataAction === "function"); } function canGetProjectColumnsMetadata() { return canUseClipItems() && !!(ppro.Metadata && typeof ppro.Metadata.getProjectColumnsMetadata === "function"); } function canGetProjectPanelMetadata() { return canInspectProject() && !!(ppro.Metadata && typeof ppro.Metadata.getProjectPanelMetadata === "function"); } function canCreateProjectMetadataSchema() { return canGetProjectPanelMetadata() && !!(ppro.Metadata && typeof ppro.Metadata.addPropertyToProjectMetadataSchema === "function" && ppro.Metadata.METADATA_TYPE_INTEGER != null && ppro.Metadata.METADATA_TYPE_REAL != null && ppro.Metadata.METADATA_TYPE_TEXT != null && ppro.Metadata.METADATA_TYPE_BOOLEAN != null); } async function canSetProjectPanelMetadata() { if (!canGetProjectPanelMetadata() || !ppro.Metadata || typeof ppro.Metadata.setProjectPanelMetadata !== "function") return false; try { const project = await ppro.Project.getActiveProject(); return !project || typeof project.lockedAccess === "function"; } catch (_) { return false; } } function canInspectColor() { return canUseClipItems(); } async function canInspectEnvironment() { if (!canInspectProject() || !ppro.Utils || typeof ppro.Utils.isAEInstalled !== "function") return false; try { const project = await ppro.Project.getActiveProject(); if (!project) return true; if (typeof project.getColorSettings !== "function") return false; const settings = await project.getColorSettings(); return !!settings && typeof settings.getGraphicsWhiteLuminance === "function" && typeof settings.getSupportedGraphicsWhiteLuminances === "function"; } catch (_) { return false; } } function canConformFootage() { return canUseClipItems(); } function canInspectSourceMonitor() { return !!(ppro.SourceMonitor && typeof ppro.SourceMonitor.getPosition === "function" && typeof ppro.SourceMonitor.getProjectItem === "function"); } function canOpenSourceMonitor() { return !!(ppro.SourceMonitor && typeof ppro.SourceMonitor.getProjectItem === "function" && typeof ppro.SourceMonitor.openProjectItem === "function" && typeof ppro.SourceMonitor.openFilePath === "function"); } function canPlaySourceMonitor() { return !!(ppro.SourceMonitor && typeof ppro.SourceMonitor.play === "function"); } function canCloseSourceMonitor() { return !!(ppro.SourceMonitor && typeof ppro.SourceMonitor.closeClip === "function" && typeof ppro.SourceMonitor.closeAllClips === "function"); } function canConfigureScratch() { return canUseProjectSettings() && typeof ppro.ProjectSettings.createSetScratchDiskSettingsAction === "function"; } function canReportWorkspace() { return !!(workspace && typeof workspace.status === "function"); } return definitions; } function validateTrackTarget(args, allowedKeys) { assertObject(args); assertOnlyKeys(args, allowedKeys.concat(["operationId"])); return { mediaType: enumValue(args.mediaType, "mediaType", ["video", "audio"]), trackIndex: nonNegativeInt(args.trackIndex, "trackIndex"), clipIndex: nonNegativeInt(args.clipIndex, "clipIndex") }; } function validateTrackItemIdentityArgs(args) { assertObject(args); assertOnlyKeys(args, ["mediaType", "trackIndex", "clipIndex", "expectedSequenceGuid"]); const result = { mediaType: enumValue(args.mediaType, "mediaType", ["video", "audio"]), trackIndex: nonNegativeInt(args.trackIndex, "trackIndex"), clipIndex: nonNegativeInt(args.clipIndex, "clipIndex") }; if (args.expectedSequenceGuid != null) result.expectedSequenceGuid = boundedString(args.expectedSequenceGuid, "expectedSequenceGuid", 512); return result; } function validateEffectAdd(args, selection) { const keys = selection ? ["mediaType", "effectId", "insertionIndex", "operationId"] : ["mediaType", "trackIndex", "clipIndex", "effectId", "insertionIndex", "operationId"]; const target = selection ? (assertObject(args), assertOnlyKeys(args, keys), { mediaType: enumValue(args.mediaType, "mediaType", ["video", "audio"]) }) : validateTrackTarget(args, keys.filter((key) => key !== "operationId")); target.effectId = boundedString(args.effectId, "effectId", 256); target.insertionIndex = args.insertionIndex == null ? null : nonNegativeInt(args.insertionIndex, "insertionIndex"); return target; } function validateEffectRemove(args, selection) { const keys = selection ? ["mediaType", "componentIndex", "expectedEffectId", "operationId"] : ["mediaType", "trackIndex", "clipIndex", "componentIndex", "expectedEffectId", "operationId"]; const target = selection ? (assertObject(args), assertOnlyKeys(args, keys), { mediaType: enumValue(args.mediaType, "mediaType", ["video", "audio"]) }) : validateTrackTarget(args, keys.filter((key) => key !== "operationId")); target.componentIndex = nonNegativeInt(args.componentIndex, "componentIndex"); target.expectedEffectId = boundedString(args.expectedEffectId, "expectedEffectId", 256); return target; } function validateSelectionUpdateArgs(args) { assertObject(args); assertOnlyKeys(args, ["mode", "expectedSequenceGuid", "items", "operationId"]); const mode = enumValue(args.mode, "mode", ["replace", "add", "remove", "clear"]); const expectedSequenceGuid = boundedString(args.expectedSequenceGuid, "expectedSequenceGuid", 512); if (mode === "clear") { if (Object.prototype.hasOwnProperty.call(args, "items")) throw commandError("UXP_INVALID_ARGUMENT", "items must be omitted when mode is clear"); return { mode, expectedSequenceGuid, items: [] }; } if (!Array.isArray(args.items) || !args.items.length || args.items.length > MAX_SELECTION_ITEMS) { throw commandError("UXP_INVALID_ARGUMENT", "items must contain 1-" + MAX_SELECTION_ITEMS + " timeline targets"); } const coordinates = new Set(), items = args.items.map((raw, index) => { assertObject(raw); assertOnlyKeys(raw, [ "mediaType", "trackIndex", "clipIndex", "expectedProjectItemId", "expectedStartSeconds", "expectedEndSeconds" ]); const item = { mediaType: enumValue(raw.mediaType, "items[" + index + "].mediaType", ["video", "audio"]), trackIndex: nonNegativeInt(raw.trackIndex, "items[" + index + "].trackIndex"), clipIndex: nonNegativeInt(raw.clipIndex, "items[" + index + "].clipIndex"), expectedProjectItemId: boundedString(raw.expectedProjectItemId, "items[" + index + "].expectedProjectItemId", 512), expectedStartSeconds: finiteNumber(raw.expectedStartSeconds, "items[" + index + "].expectedStartSeconds", 0, Number.MAX_SAFE_INTEGER), expectedEndSeconds: finiteNumber(raw.expectedEndSeconds, "items[" + index + "].expectedEndSeconds", 0, Number.MAX_SAFE_INTEGER) }; if (item.expectedEndSeconds < item.expectedStartSeconds) { throw commandError("UXP_INVALID_ARGUMENT", "items[" + index + "].expectedEndSeconds must not be before expectedStartSeconds"); } const coordinate = item.mediaType + ":" + item.trackIndex + ":" + item.clipIndex; if (coordinates.has(coordinate)) throw commandError("UXP_INVALID_ARGUMENT", "items must not contain duplicate timeline coordinates"); coordinates.add(coordinate); return item; }); return { mode, expectedSequenceGuid, items }; } function validateSelectionTargetInspectionArgs(args) { assertObject(args); assertOnlyKeys(args, ["items"]); if (!Array.isArray(args.items) || !args.items.length || args.items.length > MAX_SELECTION_ITEMS) { throw commandError("UXP_INVALID_ARGUMENT", "items must contain 1-" + MAX_SELECTION_ITEMS + " timeline targets"); } const coordinates = new Set(); return args.items.map((raw, index) => { assertObject(raw); assertOnlyKeys(raw, ["mediaType", "trackIndex", "clipIndex"]); const item = { mediaType: enumValue(raw.mediaType, "items[" + index + "].mediaType", ["video", "audio"]), trackIndex: nonNegativeInt(raw.trackIndex, "items[" + index + "].trackIndex"), clipIndex: nonNegativeInt(raw.clipIndex, "items[" + index + "].clipIndex") }; const coordinate = item.mediaType + ":" + item.trackIndex + ":" + item.clipIndex; if (coordinates.has(coordinate)) throw commandError("UXP_INVALID_ARGUMENT", "items must not contain duplicate timeline coordinates"); coordinates.add(coordinate); return item; }); } 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 validateUpdatedFields(value, required) { if (!required && value == null) return []; if (!Array.isArray(value) || !value.length || value.length > 128) throw commandError("UXP_INVALID_ARGUMENT", "updatedFields must contain 1-128 metadata field names"); const fields = value.map((item, index) => boundedString(item, "updatedFields[" + index + "]", 512)); if (new Set(fields).size !== fields.length) throw commandError("UXP_INVALID_ARGUMENT", "updatedFields must not contain duplicates"); return fields; } function validateConformanceArgs(args) { assertObject(args); const keys = [ "projectItemId", "projectItemName", "frameRate", "pixelAspectRatio", "fieldType", "removePullDown", "alphaUsage", "ignoreAlpha", "invertAlpha", "vrConform", "vrLayout", "vrHorzView", "vrVertView", "inputLutId", "operationId" ]; assertOnlyKeys(args, keys); const result = validateProjectItemTarget(args), numericEnums = ["fieldType", "alphaUsage", "vrConform", "vrLayout"]; if (args.frameRate != null) result.frameRate = finiteNumber(args.frameRate, "frameRate", 1, 240); if (args.pixelAspectRatio != null) result.pixelAspectRatio = finiteNumber(args.pixelAspectRatio, "pixelAspectRatio", 0.01, 100); for (const key of numericEnums) if (args[key] != null) result[key] = boundedInt(args[key], key, 0, 64); for (const key of ["removePullDown", "ignoreAlpha", "invertAlpha"]) if (args[key] != null) result[key] = requiredBoolean(args[key], key); if (args.vrHorzView != null) result.vrHorzView = finiteNumber(args.vrHorzView, "vrHorzView", 1, 360); if (args.vrVertView != null) result.vrVertView = finiteNumber(args.vrVertView, "vrVertView", 1, 180); if (args.inputLutId != null) result.inputLutId = boundedStringAllowEmpty(args.inputLutId, "inputLutId", 512); if (!Object.keys(result).some((key) => key !== "projectItemId" && key !== "projectItemName")) throw commandError("UXP_INVALID_ARGUMENT", "At least one conformance field is required"); 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 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 boundedUtf8StringAllowEmpty(value, name, maximumBytes) { if (typeof value !== "string" || utf8ByteLength(value) > maximumBytes) { throw commandError("UXP_INVALID_ARGUMENT", name + " must be a UTF-8 string of at most " + maximumBytes + " bytes"); } return value; } function requiredOperationId(value, name) { if (typeof value !== "string" || !/^[A-Za-z0-9._:-]{1,128}$/.test(value)) { throw commandError("UXP_INVALID_ARGUMENT", name + " must be a 1-128 character token for safe replay"); } return value; } 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 boundedInt(value, name, minimum, maximum) { if (!Number.isInteger(value) || value < minimum || value > maximum) throw commandError("UXP_INVALID_ARGUMENT", name + " must be an integer from " + minimum + " to " + maximum); return value; } function finiteNumber(value, name, minimum, maximum) { const number = Number(value); if (!Number.isFinite(number) || number < minimum || number > maximum) throw commandError("UXP_INVALID_ARGUMENT", name + " must be from " + minimum + " to " + maximum); return number; } 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 enumValue(value, name, allowed) { if (!allowed.includes(value)) throw commandError("UXP_INVALID_ARGUMENT", name + " must be one of " + allowed.join(", ")); return value; } function boundedEnumArray(value, name, allowed, maximum) { if (!Array.isArray(value) || !value.length || value.length > maximum) throw commandError("UXP_INVALID_ARGUMENT", name + " must contain 1-" + maximum + " values"); const result = value.map((item, index) => enumValue(item, name + "[" + index + "]", allowed)); if (new Set(result).size !== result.length) throw commandError("UXP_INVALID_ARGUMENT", name + " must not contain duplicates"); return result; } function requireConfirmation(value, message) { if (value !== true) throw commandError("UXP_CONFIRMATION_REQUIRED", message + "; pass confirmNonUndoable=true after review"); } function tickSeconds(value) { const seconds = value && Number(value.seconds); return Number.isFinite(seconds) ? seconds : null; } function pathEqual(left, right) { if (typeof left !== "string" || typeof right !== "string") return false; const normalizedLeft = left.replace(/\\/g, "/").replace(/\/$/, ""); const normalizedRight = right.replace(/\\/g, "/").replace(/\/$/, ""); const windowsPaths = /^(?:[A-Za-z]:\/|\/\/)/.test(normalizedLeft) && /^(?:[A-Za-z]:\/|\/\/)/.test(normalizedRight); return windowsPaths ? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase() : normalizedLeft === normalizedRight; } function valuesEqual(left, right) { return typeof right === "number" ? typeof left === "number" && Math.abs(left - right) < 0.000001 : left === right; } function commandError(code, message) { const error = new Error(message); error.code = code; return error; } return { createWorkflowDefinitions, commandError }; });