UXP's native button chrome overrides explicit `background` rules on
icon-only <button> elements -- appearance:none and background both lose.
Authored content (::before pseudo-elements) renders above native chrome,
so move all background/hover/active logic there. SVG icons and the
growing-count badge get z-index:1/2 to sit above the cover.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
UXP's SVG renderer does not draw Feather/Lucide-style stroke icons
(fill="none" stroke="currentColor"); they showed as blank/grey shapes
in Premiere. Convert all 14 panel icons to filled single-path
(Material-style) SVGs with explicit width/height attributes, which
UXP's simple-icon renderer handles reliably.
Also replace the transparent rail/icon button backgrounds with an
explicit --bg-base fill: appearance:none alone did not suppress UXP's
native grey button chrome, but an explicit background does (same trick
the working .btn rule relies on).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
UXP renders native <button> appearance (grey rounded fill) unless
appearance:none is set. .btn masked it with an explicit background,
but .rail-btn / .iconbtn use a transparent background, so the native
chrome showed through as grey blobs and suppressed the SVG icons.
Add appearance:none to both, matching the input/select reset.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace the text-heavy panel layout with a minimal icon-first UI:
a VS Code-style vertical activity rail for view switching plus a
contextual icon dock for clip actions. Every control carries a
[data-tip] label surfaced on hover via a JS-positioned tooltip
bubble (UXP's CSS engine can't be trusted with content: attr()).
All existing main.js element IDs and the JS wiring contract are
preserved; the dropped advanced section was already guarded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds an inline hi-res download trigger to the asset library.
UI:
- Small 22×22 download icon button in the top-right corner of each
asset thumbnail. Hidden by default, fades in on card hover or focus
so the resting-state grid stays clean.
- Only renders for assets that have an `original_s3_key` — proxies
and unfinished captures never offer it.
- Mirrored as a "Download original…" entry in the right-click
AssetContextMenu (between Rename and the bin actions).
Flow:
- First click (or any click while the warning is enabled) opens
DownloadWarningModal: terse copy explaining the file is the full
original ingest, can be multi-GB, and that speed depends on the
user's network connection. Footer: Cancel · Download. Body: a
"Don't show this again on this device" checkbox.
- Ticking the checkbox persists `df.lib.download.warnDismissed=1`
in localStorage. Subsequent clicks skip the modal and start the
download straight away.
Download itself reuses /api/v1/assets/:id/hires (presigned S3 URL)
— no proxy round-trip through mam-api, no in-browser progress UI
beyond what the browser already shows.
Spec: #145
Settings → Account "re-enable the warning" toggle is not in this
patch and will land separately.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ProRes / DNxHR conformed outputs are unplayable in the browser
(HTML5 video: MEDIA_ERR_SRC_NOT_SUPPORTED). The library was
referencing the ProRes original as the only source.
After the asset row is inserted, queue an H.264 proxy build the same
way services/mam-api/src/routes/assets.js does on ingest:
proxyQueue.add('generate', {
assetId,
inputKey: outputKey, // the conformed mov / mp4
outputKey: `proxies/${id}.mp4`,
});
The proxy worker writes the H.264 mp4, updates assets.proxy_s3_key,
and from then on /assets/:id/stream prefers the proxy over the
original. The library player can decode it natively.
Failure to enqueue is logged but doesn't fail the conform job — the
asset still exists and can have a proxy re-queued later.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The final concat-demux + encode step erred with:
[mp4] Could not find tag for codec prores in stream #0,
codec not currently supported in container
[out#0/mp4] Could not write header (incorrect codec parameters ?)
ProRes and DNxHR live in QuickTime (.mov), not MP4. The output path,
S3 key, and asset-row filename were all hardcoded to .mp4.
Pick the container from the codec:
prores / prores_hq / prores_4444 / dnxhr_hq → mov
h264 / h265 / anything else → mp4
outputExt is computed once at the top of the worker (before tmpfile
creation) and reused for the temp output, the S3 key
(jobs/<id>/conformed.<ext>), and the assets row's filename column.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ffmpeg 8.x removed the `ocl` shortcut option from aresample (it was a
deprecated alias for out_chlayout). The per-segment trim+normalise call
errored immediately:
[fc#-1] Error applying option 'ocl' to filter 'aresample': Option not found
Split the chain: aresample handles the sample rate, aformat asserts +
auto-converts to stereo + fltp.
aresample=48000,aformat=channel_layouts=stereo:sample_fmts=fltp
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ffmpeg 8.x's concat filter kept dying with the opaque
[fc#0] Error sending frames to consumers: Invalid argument
even after we locked fps + sample rate + pixel format + SAR in the
filter graph. Mixed sources (AV1+H.264, 23.98+60 fps, 44100+48000 Hz,
tv-range+unspecified-range pixel format) just don't survive the
concat filter cleanly in this build.
Switch to the more reliable 2-pass pattern:
1. At the trim step, re-encode each segment to a uniform intermediate
spec: libx264 ultrafast, 1920x1080 (letterboxed), yuv420p,
seqFps target rate, 48kHz stereo AAC. Per-segment ffmpeg.
2. At the concat step, use the concat *demuxer*. Because every input
now matches exactly, the demuxer is well-behaved. Transcode the
concatenated stream to the final target codec (ProRes 422 HQ etc).
Costs an extra intermediate encode (libx264 ultrafast ≈ realtime on
this hardware) but eliminates the filter-graph fragility on mixed-
source timelines, which is the workload that actually matters.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
After the demuxer → filter switch, concat still failed with
[fc#0] Error sending frames to consumers: Invalid argument
on Job 8. The filter graph normalised pixels (scale+pad+yuv420p) but
left the time-domain axes mixed:
segment-1: 23.98 fps video, 44100 Hz audio
segment-2: 60 fps video, 48000 Hz audio
segment-3: …
ffmpeg 8's concat filter requires identical frame rate + audio sample
rate + channel layout across inputs. Force them on each leg:
video: fps=<seqFps>, setpts=PTS-STARTPTS
audio: aresample=48000,
aformat=channel_layouts=stereo:sample_fmts=fltp,
asetpts=PTS-STARTPTS
setpts/asetpts re-zero each input's clock so concat's per-input PTS
window resets cleanly between segments.
Target fps comes from the sequence's frame_rate (rounded) — same axis
the sequence editor stores. Sample rate is pinned to 48000 (broadcast
standard) so the AAC encode is consistent.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ffmpeg concat demuxer dies with "Error sending frames to consumers:
Invalid argument" when input segments don't share codec / pixel format
/ framerate / resolution. Mixed-source timelines hit this every time —
e.g. an AV1 clip + an H.264 clip going through the same concat.
Switch to the concat *filter*. It re-encodes through a filter graph
so disparate inputs are normalised inline. Each input is scaled to
1920x1080 with letterbox, format=yuv420p, audio resampled. concat=n=N
joins them into [outv]/[outa].
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three cooperating bugs left the rendered output silent and in the
wrong codec:
1. executor.js trimSegment used `-frames:v` with no audio mapping.
ffmpeg dropped the audio track on each segment before they reached
the concat step. Add `-c:a copy -shortest` so each segment carries
its original audio.
2. conform.js audioFlag was `audio === 'include' ? aac : -an`. The
panel's v2.2.1 defaults send `audio: 'broadcast'`, which didn't
match 'include' → `-an` explicitly stripped audio at the encode
step. Switch to the opposite default: only an explicit 'none' or
'off' disables audio; everything else gets AAC 320k @ 48kHz.
3. conform.js video codec map only matched `codec === 'prores'`. The
panel sends `'prores_hq'` (and the conform slide panel can send
`'prores_4444'` / `'dnxhr_hq'`). All of those fell through to
libx264 and silently rendered H.264 instead of the requested codec.
Add a real codec map with the right prores_ks profiles (3=HQ,
4=4444) and DNxHR. Skip -crf for ProRes since the profile encodes
quality.
The asset-row metadata's `codec` column is normalised the same way so
the new asset record matches what was actually written.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The conform worker's final step INSERTs the rendered output into the
assets table:
INSERT INTO assets (project_id, filename, display_name, …)
VALUES ($1, …)
-- project_id NOT NULL
It reads projectId from job.data, but the /sequences/:id/conform
endpoint never set it. Render finished cleanly, ffmpeg ran, output
uploaded to S3, then the final asset row INSERT failed:
null value in column "project_id" of relation "assets"
Pass seq.project_id from the loaded sequence row. The rendered output
lands as an asset under the same project as its source sequence —
the natural target.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Panel had been sending xmeml with clipitem/name = the local Premiere
file path's basename (e.g. "dragonflight-Interstellar - Docking Scene
1080p IMAX HD.mp4"). The worker's old filename lookup ran
SELECT id, original_s3_key FROM assets WHERE filename = $1
which never matched, because the assets row's filename is the
original MAM ingest name without the "dragonflight-" prefix.
Fix: when job.data has sequenceId (always set by the conform endpoint
at routes/sequences.js:317), pull edits directly from sequence_clips,
which the panel already wrote with authoritative asset_id mappings on
push. We JOIN to assets for original_s3_key + filename and order by
(timeline_in_frames, track) so segment indices stay deterministic.
The XML is still parsed for sequence-level metadata (name, fps) when
provided, but its clipitems are no longer authoritative.
The legacy filename path (EDL input or fcpXml without sequenceId)
stays unchanged for backward compatibility.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two cooperating bugs left Export Timeline stuck at "Rendering Hi-Res"
forever:
A. worker emitted "Invalid FCP XML: no sequence element" because
Timeline.generateFcpXml produced fcpxml (FCP X schema:
<fcpxml><resources>/<library>/...) while the worker's parseFcpXml
expects xmeml (FCP 7 schema: <xmeml><sequence>...). Two completely
different formats.
Rewrite generateFcpXml to emit xmeml v5 with the structure the
parser walks:
xmeml/sequence/{name,duration,rate{timebase,ntsc},
media/video/{format/samplecharacteristics,
track[@currentExplodedTrackIndex]
/clipitem/{name,duration,rate,in,out,
start,end,file/{name,pathurl}}}}
Clipitem in/out are SOURCE frames (the underlying media in/out);
start/end are TIMELINE frames (the cut position). The worker uses
the rate timebase to parse them.
B. /api/v1/jobs/:id rejected the panel's polls with
"Invalid id — must be a UUID". The handlers below correctly parse
BullMQ-prefixed ids ("conform:42"), but router.param('id',
validateUuid('id')) ran first and 400'd everything that wasn't a
UUID. The panel's pollConform swallows the resulting fetch error
silently and polls forever.
Drop the validator. Comment in the file explains why.
Bumps panel to v2.2.2.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Contract: clicking Export Timeline does the whole pipeline with no
prompts. Behavior matches what the user actually expected from the
button label:
1. readActiveSequence — pulls the Premiere timeline + clip map
2. resolveExportProject — picks the target MAM project. First run
uses the first project on the server and caches its id in
localStorage (df.uxp.exportProjectId). Subsequent runs reuse
the cache. If the cached project was deleted server-side we
transparently re-pick.
3. Timeline.startConform with sensible defaults:
codec=prores_hq, quality=high, resolution=source, audio=broadcast
This both pushes the sequence + clip rows AND queues a real
conform job (the prior Push-to-MAM button never queued a job,
which is why "no jobs spin up" happened earlier).
4. pollConform every 2s, mapping job progress 20→95% on the
panel progress bar.
5. On completion, toast + Library.refresh() so the rendered hi-res
asset shows up in the grid without needing to click around.
The Conform slide panel stays wired for Advanced → Export & Conform
so power users can still override the codec/preset for one-off jobs.
The Push-only slide panel that this replaces is now orphaned chrome
and will be removed in a later cleanup.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
User feedback after v2.1.9: panel still chrome-heavy. The Asset Info
panel duplicates what the card already shows; 8 buttons across 3
full-width rows still claim too much vertical real estate.
Three surgical changes:
1. Drop the Asset Info details panel entirely. Card meta (name +
duration + codec) already carries everything we showed in the
key:value table. Library._showDetails / hideDetails become no-ops
so the existing call sites in main.js + library.js don't need
conditional branches.
2. Shrink .action-row .btn to 20px tall, 10.5px font, 6px horiz
padding, 3px radius. Two rows of compact buttons fit where one
bulky row used to.
3. Collapse Advanced section behind a toggle (▸ / ▾). Default
collapsed so the main 6 buttons stay the primary action surface;
click the row to expand and reveal Export & Conform / Fetch &
Relink All.
Per DESIGN.md "density over whitespace."
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
UPIA stacks every install in its own
C:\Program Files\...\UXP\Plugins\External\net.wilddragon.dragonflight.uxp_<version>\
folder without removing prior versions. After 10 deploys today there are
11 of them coexisting, and Premiere's loader can pick the wrong one,
which is why v2.1.8 didn't appear to land.
This change makes the running version visible at a glance:
- main.js reads manifest.json at runtime via require('uxp').storage
.localFileSystem.getPluginFolder() so the displayed version is
whatever Premiere actually loaded — never a hand-edited constant
that could drift.
- index.html adds #panel-version inside the status strip (between
host and ⋯) and #brand-version below the brand tag on connect.
- styles.css: small mono chip in --text-4, low key but readable.
If the chip ever shows the wrong version we know the loader picked
a stale dir; if it shows nothing the manifest read itself failed.
The install script needs to remove old _<version> dirs going forward;
the next commit will add that cleanup step to the deploy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three changes, surgical so timeline.js / conform / relink / growing
all keep working:
A. Header → 24px status strip + ⋯ menu
`connected-bar` rule kept as an alias to `.status-strip` so any code
path that still emits the old class falls through cleanly. Markup
replaced with .signal-dot + #connected-host + .btn-ghost ⋯ that
toggles a .menu containing the Disconnect button. Menu auto-closes
on outside click. Reclaims ~12px of permanent vertical chrome and
removes the always-visible Disconnect.
B. Compact action footer
`.action-row .btn` now: 22px tall, 11px font, 0.01em letterspacing.
`.advanced-section .action-row .btn` goes a step smaller (20px /
10.5px). Global `.btn` untouched so #connect-btn stays at full
weight on the connect pane.
D. Token alignment with services/web-ui DESIGN.md
--bg-0 #0B0D11 (was #0e0f12), --accent #5B7CFA (was #4f7cff),
plus the full --text-1..4 / --success / --warning / --danger / --live
palette. Legacy --ok / --warn aliased to --success / --warning so
existing rules keep resolving.
C (per-card meta) was already in v2.1.7 — no change needed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
getStartTime/getEndTime/getInPoint/getOutPoint can return null for
non-clip track items (gaps, transitions) that slip past the
getProjectItem check. Accessing .seconds on null threw a TypeError
that the outer catch swallowed — silently dropping every clip and
leaving clips[] empty, so the export panel never opened.
Also skip clips where all four time values resolve to 0 (filler items).
- _writeBuffer: catch EBUSY (Windows file-lock) and treat as success —
the file is already there from the previous import and Premiere has it
locked; no need to re-write it.
- proxy / hires: stat the destination first; if the file already exists
skip the download entirely and go straight to importIntoProject.
- importIntoProject: importFiles returning false means the file is
already in the Premiere project — not an error, treat as success.
img.src direct assignment never sends Authorization headers, so all
thumbnail requests returned 401 once the global auth gate was enabled.
Now fetches via API.request(), converts response to a blob URL, and
assigns that to img.src. Falls back to the placeholder div on error.