Commit graph

839 commits

Author SHA1 Message Date
354731a363 fix(capture): fix DeckLink device name enumeration for SDI port 2+; add per-take project selector on Recorders page
- capture-manager.js, routes/capture.js: fix ffmpeg -sources decklink
  parse regex from v4l2 hex-address format (never matched DeckLink output)
  to correct indented-line format. Port 2+ (index 1+) was falling through
  to a wrong model-name fallback, causing ffmpeg to open the wrong input
  and produce black frames. Now logs the detected device list and the
  selected name at start.

- recorders.js (/start): accept per-take projectId override in request
  body. If provided, clips go to that project instead of the recorder's
  default project_id. Used for both the live-asset INSERT and the
  PROJECT_ID env var passed to the capture container.

- screens-ingest.jsx (RecorderRow): add project dropdown shown when
  recorder is stopped. Defaults to the recorder's configured project;
  operator can change it before hitting Record without editing the
  recorder config.
2026-05-28 22:26:08 +00:00
Claude
1fcb927d26 feat(web-ui): library Download button + dismissable size warning (#145)
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>
2026-05-28 16:14:24 -04:00
Claude
6bc6478270 feat(worker): conform — queue proxy build for the conformed output
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>
2026-05-28 15:49:01 -04:00
Claude
446a563647 fix(worker): conform — write ProRes/DNxHR to MOV, not MP4
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>
2026-05-28 15:40:53 -04:00
Claude
71d8944a01 fix(worker): conform — use aformat for channel layout (ffmpeg 8 dropped aresample ocl)
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>
2026-05-28 15:37:35 -04:00
Claude
686b90294b fix(worker): conform — 2-pass strategy (normalise on trim, demux on concat)
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>
2026-05-28 15:34:52 -04:00
Claude
fcf4c8bbe7 fix(worker): conform — lock fps + sample rate in concat filter graph
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>
2026-05-28 15:21:23 -04:00
Claude
94b6710e2d fix(worker): conform — concat-filter for mixed source formats
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>
2026-05-28 15:04:55 -04:00
Claude
6412b5c252 fix(worker): conform — preserve audio + map ProRes/DNxHR codecs
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>
2026-05-28 14:32:49 -04:00
Claude
56d7479a35 fix(mam-api): pass project_id into conform job so render can register the asset
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>
2026-05-28 14:24:04 -04:00
Claude
aeecb6e32a fix(worker): conform — resolve clips from sequence_clips instead of filename
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>
2026-05-28 14:08:49 -04:00
Claude
0abef056e7 fix(uxp+mam-api): Export Timeline render — xmeml schema + BullMQ job poll
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>
2026-05-28 13:58:13 -04:00
Claude
540d333758 feat(uxp): v2.2.1 — Export Timeline is now one-click push + render → asset
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>
2026-05-28 13:28:51 -04:00
Claude
e4e69973e5 ui(uxp): v2.2.0 — density pass: drop details panel, tighter buttons, collapsible Advanced
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>
2026-05-28 13:03:56 -04:00
Claude
c3b087020d ui(uxp): v2.1.9 — visible version chip + diagnose multi-version install
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>
2026-05-28 12:00:50 -04:00
Claude
c2a6c1557b ui(uxp): v2.1.8 — density redesign on top of v2.1.7
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>
2026-05-28 11:51:47 -04:00
Dragonflight Deploy
b04882a310 release(uxp): v2.1.7 — fix resource busy + export timeline robustness 2026-05-28 11:12:49 -04:00
60e5093c6b fix(uxp): null-safe Time object access in readActiveSequence
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).
2026-05-28 11:12:32 -04:00
382f432693 fix(uxp): resolve "Resource busy" on re-import of same asset
- _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.
2026-05-28 11:11:32 -04:00
Dragonflight Deploy
e533566ae2 release(uxp): v2.1.6 — fix thumbnail auth (bearer fetch + blob URL) 2026-05-28 09:53:34 -04:00
c3e4306d9f fix(uxp): fetch thumbnails via API.request() to carry Bearer token
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.
2026-05-28 09:41:26 -04:00
eeb0d9f65f UXP v2.1.5b: main.js relink handler await fix + artifact 2026-05-28 09:15:24 -04:00
7f0ca5922f UXP v2.1.5: main.js — fix relink handler: await getActiveProject, use requestExternal + arrayBuffer 2026-05-28 09:15:06 -04:00
469521d524 UXP v2.1.5 release artifact 2026-05-28 09:07:15 -04:00
07840441b9 UXP v2.1.5: bump manifest version 2026-05-28 09:07:00 -04:00
5774f61ac7 UXP v2.1.5: timeline.js — await all premierepro calls; runtime is async 2026-05-28 09:06:43 -04:00
1fb790a569 UXP v2.1.5: import-flow — await all premierepro calls (runtime is async despite docs saying sync) 2026-05-28 09:05:49 -04:00
11cb93aa51 UXP v2.1.4 release artifact 2026-05-28 08:32:55 -04:00
dbc67636b2 UXP v2.1.4: bump manifest version 2026-05-28 08:32:35 -04:00
460b590d46 UXP v2.1.4: timeline.js — replace API.requestFollow with API.requestExternal for batch relink S3 downloads 2026-05-28 08:32:18 -04:00
f3a640a7c5 UXP v2.1.4: api.js — remove redirect:manual (not supported in UXP fetch); UXP auto-follows redirects 2026-05-28 08:31:09 -04:00
baa289f6c3 UXP v2.1.4: import-flow — drop redirect:manual (not supported in UXP fetch, causes null body); use arrayBuffer() fallback if body.getReader unavailable 2026-05-28 08:30:30 -04:00
e5f218655e UXP v2.1.3 release artifact 2026-05-28 07:50:52 -04:00
8119b57b45 UXP v2.1.3: bump manifest version 2026-05-28 07:50:29 -04:00
9765fd91f7 UXP v2.1.3: main.js — fix inline relink handler (sync getActiveProject, no require inline) 2026-05-28 07:50:18 -04:00
a25e4b6071 UXP v2.1.3: timeline.js — correct Premiere DOM API calls per official docs 2026-05-28 07:48:57 -04:00
046d99f57a UXP v2.1.3: import-flow — use window.path.join, replace fs.createWriteStream with fd-based chunked write 2026-05-28 07:47:44 -04:00
fcc737e05b UXP v2.1.2 release artifact 2026-05-28 07:20:14 -04:00
8f93302f45 UXP v2.1.2: bump manifest version 2026-05-28 07:20:09 -04:00
17ca9bfc75 UXP v2.1.2: import-flow — replace path.join (not in UXP) with manual join helper 2026-05-28 07:19:46 -04:00
f8fa0fa010 UXP v2.1.1 release artifact 2026-05-28 02:25:30 -04:00
907058de83 UXP v2.1.1: bump manifest version 2026-05-28 02:25:21 -04:00
bfe0316067 UXP v2.1.1: add conform-proj-select to Conform panel 2026-05-28 02:24:58 -04:00
5d94838830 UXP v2.1.1: main.js — fix recordImport from proxy/hires return values, add conform project select, fix conform panel 2026-05-28 02:24:07 -04:00
76fff5efc2 UXP v2.1.1: import-flow.js — expose _tempPath/_streamToFile, return {localPath,safeName} from proxy/hires 2026-05-28 02:22:58 -04:00
5432c2dfa1 UXP v2.1.0 release artifact 2026-05-28 02:21:36 -04:00
b3b2655272 UXP v2.1.0: bump version in manifest 2026-05-28 02:20:43 -04:00
16366267c4 UXP v2.1.0: main.js — full rewrite, wire all panels, tabs, export, conform, relink, mount live 2026-05-28 01:01:29 -04:00
066718c968 UXP v2.1.0: timeline.js — new module: sequence read, FCP XML, export, conform, batch relink via UXP premierepro API 2026-05-28 01:00:19 -04:00
60d0b09c63 UXP v2.1.0: ui.js — add formatDuration, sanitizeFilename, slide panel helpers, escapeXml 2026-05-28 00:59:21 -04:00