YouTube now packs AV1 inside .mp4, so the old `bv*[ext=mp4]` format selector
grabbed AV1 for the downloaded ORIGINAL. Premiere rejects AV1 on hi-res import
("unsupported file type"). The h264 proxy was fine, but the original wasn't.
Prefer avc1 (H.264) so the original is Premiere-native, falling back to the
previous any-mp4 behaviour only when no H.264 rendition exists. avc1 tops out
at 1080p on YouTube (above that is AV1/VP9 only), which is the universally
importable ceiling anyway.
Verified on a real URL: old selector -> av01 1080p; new selector -> avc1 1080p
(same resolution, now Premiere-native).
Co-Authored-By: Claude Opus 4.8 <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>
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>
executor.js:
- transcodeVideo() now accepts videoMinRate, videoMaxRate, videoBufSize
- When set, passes -minrate/-maxrate/-bufsize to FFmpeg for ABR/VBR mode
- libx264 operates with per-scene quality variation within the envelope
proxy.js:
- Target average: 750k (gpu_bitrate_mbps=0.75)
- Min: 375k (50% of target), Max: 998k (~133%), Buffer: 2× max
- Gives effective range of ~500k-1M depending on scene complexity
- Log now shows VBR min-max-avg
- GPU fallback also passes VBR params
- Default videoBitrate changed from 10M to 750k in executor.js
- GET /assets/:id/stream now returns a signed S3 URL directly (4h TTL)
instead of pointing to the /video pipe endpoint. Browser streams
directly from S3 — no Node.js bottleneck, S3 handles range requests
natively for smooth seeking.
- GET /assets/:id/video now redirects (302) to a signed S3 URL.
Belt-and-suspenders: any code still calling /video gets redirected.
- proxy.js: default bitrate changed from 10Mbps to 1.5Mbps, audio
default from 192kbps to 128kbps. DB settings already updated to
1.5Mbps. Cuts proxy file size ~6x for the same quality content.
Existing proxies need re-generation at new bitrate.
Root causes found:
1. Scheduler crashing every 15s: assets table has no error_message column.
Fix: remove error_message from UPDATE in scheduler.js (#66 regression).
2. Clip freezing: client-side filmstrip seek loop runs on main thread,
seeks same proxy the player is streaming → both stall → freeze.
Fix: replace browser seek loop entirely with server-side FFmpeg worker.
3. No dedicated filmstrip worker: filmstrip was never pre-built server-side.
Changes:
- services/mam-api/src/db/migrations/018-add-filmstrip-s3-key.sql
Add filmstrip_s3_key TEXT column to assets table
- services/worker/src/workers/filmstrip.js (new)
BullMQ worker: downloads proxy, runs FFmpeg fps filter to extract
28 evenly-spaced JPEG frames, base64-encodes them, uploads JSON
array to S3 at filmstrips/<assetId>.json, stores key in DB
- services/worker/src/workers/thumbnail.js
Queue filmstrip job automatically after thumbnail completes
- services/worker/src/index.js
Register filmstrip worker (concurrency=2), export filmstripQueue
singleton, close it on SIGTERM
- services/mam-api/src/routes/assets.js
- filmstripQueue added
- POST /reprocess?type=filmstrip now supported
- GET /:id/filmstrip returns signed S3 URL for JSON frames
- services/mam-api/src/routes/jobs.js
filmstrip queue visible in Jobs UI
- services/web-ui/public/screens-asset.jsx
Replace browser seek loop with fetch of /assets/:id/filmstrip
→ fetch S3 JSON → render frames. Zero browser-side video seeking.
Right-click and Files tab re-generate via API endpoint.
track?.@_currentExplodedTrackIndex is invalid JS syntax — @ is not a
valid identifier character. Replaced with track?.['@_currentExplodedTrackIndex']
so the worker process no longer crashes on startup.
Adds Ingest → YouTube. UI takes a URL + project, API enqueues a BullMQ
"import" job, worker shells out to yt-dlp, lands the MP4 in S3 at the
same originals/{assetId}/... path uploads use, then hands off to the
existing proxy queue. Imported assets share one lifecycle with uploads
from that point on.
Worker container picks up yt-dlp + python3 (apk on alpine, apt on the
GPU variant). The new 'import' queue is registered in jobs.js so it
appears in the Jobs SSE stream and retry/delete work for free.
Spec: docs/superpowers/specs/2026-05-23-youtube-importer-design.md
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
path instead of failing the video transcode
Previously IMAGE_CODECS contained the raster ffprobe codec names ('png',
'mjpeg', 'jpeg', 'webp', 'gif', 'tiff', 'bmp', 'jpegls') but not 'svg'.
An SVG-as-asset (e.g. an architecture diagram dragged into a project) was
correctly tagged media_type='image' in the DB but ffprobe reported its
codec as 'svg', which fell through to the video branch, found
durationMs===null, and died with 'Empty or truncated source: codec=svg,
resolution=0x0'. That clogs the failed-jobs list with red rows that have
nothing to do with broken captures.
Two fixes here:
1) Add 'svg' to IMAGE_CODECS so the existing transcodeImage()/poster
path handles it.
2) Also bail to the poster path when the asset row itself says
media_type='image', even if ffprobe didn't return a codec name we
recognize (defensive — catches future formats like AVIF without
requiring an explicit catalog update).
Closes part of #13.
Proxy failures ("moov atom not found"):
- root cause: failed/aborted SRT/RTMP recordings still uploaded 0-byte
(or ftyp-only ~1KB) objects to S3, which ffmpeg can't probe
- worker proxy.js now bails on inputs < 4 KiB with a clear message
before handing the file to ffmpeg
- capture-manager.stop() returns framesReceived + empty flag
- capture shutdown handler skips POST /assets entirely on empty
sessions, instead calls new POST /assets/:id/mark-empty to flip
the pre-created live asset to 'error' with a note
Library asset right-click menu:
- new AssetContextMenu component on screens-library.jsx; right-click
any asset in grid or list view to open
- actions: Open, Rename, Move to bin (lists up to 10 bins), Remove
from bin, Copy asset ID, Delete permanently (hard=true)
- viewport-aware positioning (won't clip past window edges)
- dismisses on outside click / contextmenu / scroll
- Library now refreshes via /assets after mutations; normalizeAsset
exposed on window so the re-fetch shape matches boot
- ctx-menu styles in styles-rest.css
- Settings: drop AMPP tab, rename GPU/Transcoding → Proxy encoding
with explicit 'applied to every ingested file' wording, expose
CPU codec/preset options when GPU is off
- New Capture SDKs tab (Settings): upload Blackmagic / AJA / Deltacast
SDK archives (.zip / .tar.gz) staged to /sdk/<vendor>/ inside mam-api;
BMD is fully wired into the FFmpeg build pipeline, AJA + Deltacast
staging-only pending FFmpeg patches
- mam-api: new /api/v1/sdk routes (multer upload, extract, list, delete);
Dockerfile gets unzip+tar; docker-compose mounts /mnt/NVME/MAM/sdk:/sdk
- proxy worker now reads proxy-encoding settings from DB on every job,
builds args for libx264 / NVENC / VAAPI, falls back to libx264 on
hardware-encode failure
- settings GET /s3 falls back to S3_* env vars when DB is empty so the
UI reflects what's actually wired (fixes 'not configured' false alarm)
If the thumbnail job throws (network blip, ffmpeg error, short clip), the
asset was left stuck in status='processing' indefinitely. Since the proxy
already exists and the asset is playable, set status='ready' in the catch
block before re-throwing so BullMQ can still record the failure.
The jobs table row no longer exists for conform jobs (POST /jobs/conform
now goes directly to BullMQ). The UPDATE queries were no-ops (WHERE id = NULL)
so they're safe to remove. BullMQ tracks completed/failed status itself.