UXP: Adobe's Beautiful Promise
Adobe said it was simple. "Build plugins with HTML, CSS, and JavaScript," they said. "It's just a web page inside Premiere." And in a way, that's true — until it isn't.
UXP (Unified Extensibility Platform) is Adobe's modern runtime that replaced CEP (Chrome Embedded Framework) and old ExtendScript across Premiere Pro, Photoshop, InDesign, and others. It runs a proprietary JavaScript engine — not V8, not full Node.js — with access to native app APIs through specific modules like require('premierepro').
The pitch is solid: you write HTML + vanilla JS, use real CSS, communicate with Premiere through a documented API, and ship your plugin as a .ccx file. No C++, no deep SDK. For a video editor who knows a bit of JavaScript, it sounds like the perfect entry point.
What Adobe Forgot to Mention
Here's where things get interesting. UXP is a sandboxed environment. It's not a browser. It's not Node.js. It's its own thing — and it has some very specific holes where features you'd expect simply don't exist.
I found these out the hard way, usually when Premiere Pro crashed, froze, or silently did nothing.
Works ✓
- require('premierepro')
- require('fs') — async
- fetch('https://...')
- navigator.language
- TextDecoder / Encoder
- BigInt
- uxp.shell.openExternal()
Breaks ✗
- new Worker(...)
- AudioContext
- fs.readFileSync
- fetch('file://...')
- WebAssembly + pthreads
- SharedArrayBuffer
- require('child_process')
That last one — WebAssembly with pthreads — deserves its own story. I tried using a WASM-based MP3 decoder. Premiere Pro crashed immediately, every single time, with zero error messages. It took me days to figure out why. I'll get to that in a moment.
Reading an Audio File Should Be Simple
My plugin needs to read an audio file — a WAV or MP3 from the user's drive — to detect the BPM. Sounds simple. In any browser, in any Node.js app, you'd do this in 5 lines. In UXP, there are two specific traps.
Trap #1: readFileSync doesn't exist
In UXP, fs.readFileSync throws Route not found. There's no sync filesystem API. Everything is async/await. That's fine — just make sure you're not copying Node.js patterns without thinking.
Trap #2: The buffer is a fake
When fs.readFile() returns, you don't get a real ArrayBuffer. You get a UXP proxy ArrayBuffer — an object that looks like an ArrayBuffer but behaves differently when you try to process it with audio decoding code. The fix is to copy it into a real native buffer before doing anything else:
const raw = await fs.readFile('/path/to/audio.wav');
// ⚠️ raw is a UXP proxy — copy it first!
const byteLength = raw.byteLength ?? raw.length;
const nativeBuffer = new ArrayBuffer(byteLength);
new Uint8Array(nativeBuffer).set(new Uint8Array(raw));
One more thing: if your plugin reads files, you must declare it in manifest.json. Otherwise the permission is silently denied:
"requiredPermissions": {
"localFileSystem": "fullAccess"
}
The Markers That Lie to You
Once you have your BPM and beat timestamps, you need to create markers on the clip in Premiere. The Markers API looks clean in the docs. In practice, there are a few things that will waste your time.
Always use getter methods
This one burned me. If you try to access marker properties directly, they return undefined:
marker.name // ❌ undefined — always
marker.colorIndex // ❌ undefined — always
marker.getName() // ✅
marker.getColorIndex() // ✅
marker.getStart() // ✅ → TickTime
marker.getDuration() // ✅
Cast first, then get markers
To access markers on a source clip, you can't just call getMarkers(projectItem) directly — it returns null. You have to cast the item first:
// ❌ This returns null
const markers = await ppro.Markers.getMarkers(projectItem);
// ✅ Cast first
const clipPI = ppro.ClipProjectItem.cast(projectItem);
const clipMarkers = await ppro.Markers.getMarkers(clipPI);
Premiere's 8 native marker colors
Marker colors are set by index — there's no free color picker. Here are the 8 available colors:
| Index | Color | Hex |
|---|---|---|
| 0 | 🟢 Green | #4aad4a |
| 1 | 🔴 Red | #d53a3a |
| 2 | 🟣 Purple | #a06db5 |
| 3 | 🟠 Orange | #e8832a |
| 4 | 🟡 Yellow | #d4a017 |
| 5 | ⚪ White | #d0d0d0 |
| 6 | 🔵 Blue | #4084e5 |
| 7 | 🩵 Cyan | #1ab3a6 |
50 Is the Magic Number
Every write to the Premiere Pro API must happen inside an executeTransaction() call. That's fine — it enables undo history. But there's a limit nobody documents: roughly 50 actions per transaction. Go over that limit and Premiere hangs silently.
The fix is batching. If you're creating 200 markers, split them into 4 transactions of 50:
const BATCH_SIZE = 50;
for (let i = 0; i < beats.length; i += BATCH_SIZE) {
const batch = beats.slice(i, i + BATCH_SIZE);
await project.executeTransaction(async (ca) => {
for (const beat of batch) {
ca.addAction(
clipMarkers.createAddMarkerAction(
`[BM] ${beat.index}`,
'Comment',
ppro.TickTime.createWithSeconds(beat.time),
ppro.TickTime.createWithSeconds(0),
'beatmarker-guid'
)
);
}
}, `Add beat markers — batch ${Math.floor(i / BATCH_SIZE) + 1}`);
}
continue inside a transaction callback. It can behave unpredictably. Pre-filter your array before entering the transaction loop.
CSS Inside Premiere Is a Different Dimension
UXP renders a real HTML/CSS interface, which is great. But there are some specificity rules that don't behave the way you'd expect from a browser.
The most painful one: ID selectors with !important override class selectors with !important. That sounds normal — until you're building a dynamic UI and wondering why your toggle button won't change color no matter what class you add to it.
The safest solution for dynamic visual state: use inline styles instead of class toggling:
// ❌ This might not work — class selectors lose to ID !important
beatBox.classList.add('active');
// ✅ This always works
beatBox.style.backgroundColor = '#d53a3a';
beatBox.style.color = '#fff';
One more UXP CSS gotcha: line-height must be at least 1.5 on small italic text, otherwise descenders get clipped by the rendering engine.
The MP3 Problem: WASM is a Trap
My plugin needed to support MP3 files. WAV was easy — I wrote a pure JavaScript decoder using DataView. But MP3 is complex, and I figured I'd use a proper library.
Here's what I tried, and what happened:
| Approach | Result in UXP |
|---|---|
| WASM with pthreads | 💥 Crashes Premiere instantly |
| mpg123-decoder (single-thread WASM) | 💥 Still crashes Premiere on analysis |
| AudioContext.decodeAudioData | 🚫 Doesn't exist in UXP |
| Web Workers | 🚫 typeof Worker === 'undefined' |
| js-mp3 (pure JS) | ✅ Works perfectly |
The solution was js-mp3 — a 100% pure JavaScript MP3 decoder. No WASM, no Workers. Just math and arrays. It's slower, but it works inside UXP without crashing anything.
There's also a timing correction you have to apply. The js-mp3 decoder introduces a startup delay of 2070 samples from the MDCT warm-up phase. Add the encoder delay from the file's Xing/LAME header, and you have to strip those samples from the output before running beat detection — otherwise every beat timestamp will be slightly off.
Packaging Without Losing Your Mind
The manifest.json has some gotchas that aren't obvious from the docs:
| ❌ Wrong | ✅ Correct |
|---|---|
| "app": "PPRO" | "app": "premierepro" (lowercase) |
| "host": [{ ... }] | "host": { ... } (object, not array) |
| missing "main" | "main": "index.html" (required) |
| missing launchProcess | needed for shell.openExternal() |
If you're using npm packages that depend on Node.js modules missing in UXP — like worker_threads, vm, or url — you'll need to bundle with esbuild and inject stubs for the missing modules. The plugin folder needs to be self-contained: no node_modules, just your bundled JS.
Distribution is a .ccx file. During development, load it via the UXP Developer Tool (UDT). For production, users double-click the .ccx with Premiere closed. If you want to list on the Adobe Exchange, prepare for a review process that takes days to weeks.
Was It Worth It?
Absolutely. BeatMarker now runs inside Premiere Pro, detects BPM from any WAV or MP3 file, and creates color-coded markers on source clips — one click instead of an hour of manual work on every project.
UXP is genuinely a good platform. The frustrating parts aren't because it's badly designed — they're because the documentation doesn't cover production realities. Hopefully this article does.
All the source files, specs, and changelogs that inspired this article live in the open-source repo. Feel free to dig in.