raw2bmx v1.6 does not have a --growing-file option; using it causes
'Unknown Input Option' and immediately crashes the pipeline. The
--part interval alone is sufficient — body partitions with updated
IndexDuration are written every 30 frames, and the file has no footer
(open state) while recording, which is what Premiere's growing-file
reader polls for.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Without --growing-file, raw2bmx writes body partitions via --part but
does NOT mark them as closed partitions with self-contained index table
segments. Premiere Pro's growing-file reader requires closed partitions
to safely parse an in-progress MXF and detect that the duration has
advanced — without this flag the file imports fine but never shows
growth in the timeline.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Added useEffect to parse location.hash and update route state.
Fixes deep links like /#/library not rendering correct screen.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Cap monitor column at 960px width so full GUI fits 1920x1080 without scroll.
Preview now ~960×540px (16:9), leaves room for 300px rail + margins.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
ffmpeg's mxf muxer cannot write a growing file — its header/index duration
stays N/A until the footer at close (proven: file grows on disk but readable
duration never advances), so Premiere never sees growth. Replace the growing
master muxer with bmx/raw2bmx --growing-file, the reference growing-OP1a writer.
Capture image builds bmx (bbc/bmx v1.6) from source (bmxlib-tools absent in
bookworm). Growing pipeline: one ffmpeg decodes SDI -> split into MPEG-2 422
essence + PCM (to named FIFOs) + the H.264 HLS preview; raw2bmx muxes the
growing OP1a MXF to the share, updating IndexDuration incrementally. FIFO
open-order deadlock fixed by parent-priming both FIFOs. Stop forwards SIGINT
so ffmpeg EOFs and raw2bmx finalizes the footer; stop() awaits raw2bmx exit
before the promotion worker uploads. Raster/fps -> raw2bmx essence flag via
deriveGrowingRaster (default 1080i59.94).
Proven live (zampp2): IndexDuration grows 43->223->403 frames at 3/8/15s
mid-write (ffmpeg stayed N/A); finalized file valid; HLS preview intact.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The promotion worker promoted on mtime-idle (>=8s), but CIFS attribute caching
makes an actively-growing MXF look idle, so it grabbed the live file ~15s into
recording, uploaded it, flipped the asset live->ready, and unlinked it ("a
worker is stealing the file"). Gate promotion on the recorder's live status:
the growing asset's display_name is the recorder's current_session_id, so skip
promotion while a recorder with that session is status='recording'. Only
promote once recording has stopped.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Growing root cause (4th attempt): Premiere doesn't import H.264-in-.ts
("unsupported compression type"); its growing-file support is MXF OP1a.
Prior MXF/DNxHR failed because DNxHR is VBR and never flushes the incremental
index — XDCAM HD422 (mpeg2video, CBR) DOES write index segments into body
partitions mid-record (proven live via SIGKILL: 5 index segments, readable,
no footer). Growing master is now MXF OP1a / XDCAM HD422 4:2:2 CBR + PCM s16le,
operator bitrate as CBR (default 50M). live-path returns .mxf to match.
GUI: bitrate input is now always editable in growing mode (was hidden for
ProRes-selected codecs); codec menu shown disabled-with-explanation under
growing (it had only looked "missing" due to a stale served bundle).
Requires Premiere prefs: Media > "Automatically refresh growing files" ON,
and disable the two XMP-write-on-import options.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
screens-playout.jsx declared a top-level function fmtDuration(secs) that, in
the shared global script scope, overwrote data.jsx's fmtDuration(ms). After
the playout redesign loaded, normalizeAsset(duration_ms) hit the seconds-based
version, rendering every asset duration x1000 (15000ms shown as 4:10:00).
Rename the playout-local helpers to playoutFmtDur/playoutFmtTC.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Growing master was H.264 High 4:2:2 Intra (high422/yuv422p) — ffprobe/VLC
open it but Adobe Premiere's H.264 importer only accepts 8-bit 4:2:0, so it
refused ("won't import"). Switch growing video to -profile:v high
-pix_fmt yuv420p (still -g 1 all-intra). Also the growing branch ignored the
operator's bitrate; now applies -b:v/-maxrate/-bufsize. Modal notes that
growing mode fixes codec/container (bitrate still applies).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
duration_ms/file_size are int8; node-postgres returned them as strings,
a footgun for any consumer doing arithmetic/sorting/comparison (already
hand-patched once in playout totals). Register a global int8 type parser
so the API emits real numbers. All such values are < 2^53 (no precision loss).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add v2.2.3 to downloads (streaming write fix for large imports)
- Fix duration bug: worker now overwrites with ffprobe result instead of preserving capture wall-clock estimate
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Drop in the redesigned timeline-centric Playout (PGM monitor, transport,
SCTE-35 card, as-run drawer) from the on-node redesign, fully wired to the
real playout API (channels/transport/HLS preview w/ error-recovery/as-run);
no mock data. In-page ConfirmModal for destructive actions.
SCTE-35: new playout_scte_breaks table (migration 033), endpoints to
schedule/trigger/list/cancel breaks (POST/GET/DELETE /channels/:id/scte[/trigger]),
scheduler due-break sweep, engine triggerScte + auto-return + as-run 'scte'
rows + on-air SCTE-BREAK state and timeline AD markers. In-stream SCTE-35
cue injection is a documented stub (CasparCG FFMPEG consumer exposes no
scte35 muxer) — scheduling/triggering/countdown/as-run are functional.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Root cause: MXF OP1a writes its index/duration only in the footer partition
on finalize, so a growing MXF has no footer and VLC/Premiere/ffmpeg-strict
refuse it ("Unable to open file on disk"). Separately the proxy job pointed
at a .mov S3 key that never existed (promotion worker watched a local empty
disk, not the SMB share), so stop -> instant proxy failure.
Fix: growing master is now MPEG-TS (H.264 high422 all-intra + AAC), which is
readable from the first PAT/PMT while still growing (verified mid-write decode).
hiresKey derives from the actual produced extension. Capture skips finalize for
growing recorders (leaves asset live for promotion). Promotion worker CIFS-
mounts the same growing_smb share before scanning; worker image gets cifs-utils
and worker-p4 runs privileged (local /growing bind removed). /live-path uses .ts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When growing_enabled=true the capture container writes the master to
/growing/{projectId}/{clipName}.{ext} instead of streaming it to S3.
The capture container's graceful-shutdown handler (running during the
Docker stop) calls POST /assets/:id/finalize with the expected S3 key,
which queues a proxy job. That key never had data in S3 so the proxy
worker downloaded an empty object and failed with 'unable to open file'.
Fix: in the stop endpoint, after the container has exited (meaning
ffmpeg has finished flushing the growing file), upload the growing file
to S3 from the mam-api node (which has /growing mounted). The upload
completes before the HTTP response is sent, so by the time the client
refreshes and the BullMQ worker dequeues the proxy job the S3 object
exists.
Also handles the edge case where finalize already ran and flipped the
asset to 'processing' — we still do the upload so the already-queued
proxy job can succeed. Best-effort: a missing growing file (empty
recording or SMB-path scenario) is logged but does not fail the stop."
#162 local-spawn stop now uses /stop?t=180 + waits for asset to leave 'live'
before removing the container (no more SIGKILL-corrupted masters / stuck-live).
#163 validateRecorderConfig guard (PCM!=MP4, HEVC!=MXF, NVENC needs GPU) on
create+PATCH; codec presets in new-recorder modal.
#159 container list reads Docker /stats memory (N/A when null) + UI render.
#160 primary node self-populates version + uptime on the Cluster screen.
#145 asset-detail Download original gated by dismissable size warning.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The growing edit-while-record file was a fragmented MOV (empty moov), which
Premiere can't open ("Unable to open file on disk"). Write the growing master
as MXF OP1a / DNxHR HQ (Premiere-native, growable on disk); finalized master
keeps today's non-fragmented +faststart MOV.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace 17 native window.confirm() destructive prompts with an in-page
ConfirmModal/useConfirm (added to visuals.jsx) across jobs/asset/editor/
ingest/projects/admin/playout/library. Add "Created by Wild Dragon LLC"
footer to the home launcher.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per-node "Capture Drivers / SDKs" panel installs Blackmagic / AJA / Deltacast
/ NDI drivers without SSH. node-agent gains NODE_TOKEN-gated /driver/install
+ /driver/status (spawns a one-shot privileged ubuntu container that bind-
mounts host kernel paths + the repo and runs deploy/install-driver.sh);
mam-api adds admin-gated /cluster/:id/install-driver + /driver-status.
Driver files live in-repo under sdk/<vendor>/ (private repo); binaries are
admin-supplied per each sdk/<vendor>/README.md. Vendor allowlist throughout.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The hi-res master was streamed to S3 over a non-seekable pipe, which forced
a fragmented MOV (+frag_keyframe+empty_moov) with empty stco/stsz sample
tables — Premiere reports "file cannot be opened". Now: fragmentation only
for the growing SMB file; finalized master writes to a seekable local temp
with +faststart, stop() awaits ffmpeg exit to flush the moov, then uploads
the finalized file and cleans up.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Repo is private/internal — vendor the DeckLink SDK headers (Linux/include)
under services/capture/sdk/ so the capture ffmpeg build is self-contained
instead of operator-supplied. Runtime libDeckLinkAPI.so (from DesktopVideo
driver) remains uncommitted.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
onboard-node.sh auto-detects GPU (nvidia-smi/lspci) and SDI capture cards
(blackmagic/deltacast) and computes PROFILES (worker [+gpu] [+capture])
automatically; explicit NODE_ROLE/PROFILES still override. Add Node wizard
drops the role picker — node self-configures from hardware.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Playout preview: add hls.js ERROR handler (recover media/network errors,
resume on stall) + live-edge tuning — first transient error no longer halts
<video> to black. Purge stale HLS segments before re-mux (re)start so a
prior/duplicate sidecar session can't corrupt the live playlist.
Growing files: normalize growing_smb_mount (smb://, \host\share, host/share)
to CIFS UNC //host/share in capture-manager — mount no longer fails and
falls back to S3.
Monitors: surface playout channels as monitor tiles (live HLS on-air,
idle placeholder otherwise) in a labeled Playout group.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The UXP panel icon-rail redesign (v2.2.2 .ccx + premiere-plugin-uxp src) is
already present in main, identical, via later panel work. The branch's only
remaining delta is a stale screens-admin/editor/data.jsx (~685 lines behind
main's newer admin code, incl. the Add Node wizard). Recorded as merged with
-s ours to avoid reverting newer work; main tree unchanged.
All fixes from this May-17 branch (SRT/RTMP ffmpeg spawn, yuv420p thumbnail
pix_fmt, proxy-from-hires enqueue, recorder field aliases) already exist in
main via later work. Recorded as merged with -s ours; main tree unchanged.
Cluster: AddNodeModal on Admin->Cluster mints a node token via /auth/tokens
and emits a ready-to-paste curl|bash onboarding command. New admin-only
GET /cluster/onboard-info returns apiUrl/scriptUrl/branch. Role->PROFILES
mapping (worker/capture/gpu); gate worker-l4 behind compose profile [gpu].
Home: restore "Let's Create" kicker + one-line "Media Asset Management &
Production Platform" tagline; animated accent pulse behind the dragon logo
(reduced-motion safe); move Settings tile to a centered bottom row.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Dashboard (screens-home.jsx): rebuild to new design, fully live-wired.
Dropped fabricated figures per "real data" rule (object-store %, uptime,
storage breakdown); repurposed ingest cell to real Assets-24h count.
Fixed undefined refs and double-rendered Resources section.
Playout: as-run writer in scheduler.js writeAsRun() off the health-tick
/status poll; AsRunPanel UI + missing CSS in styles-playout.css.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 720p30 libx264 preview re-encode couldn't sustain real time on the
CPU-only sidecar (running alongside CasparCG's mixer + STREAM consumer):
the UDP input overran and the HLS output stalled, freezing the playlist so
hls.js saw a static live edge (monitor black). Drop the confidence monitor
to 360p / 20fps / ultrafast (-g 40 = 2.0s GOP) — a fraction of the cost,
sustains real time comfortably. NVENC would be ideal but the image's static
ffmpeg has no nvenc encoder; 360p ultrafast is plenty for a preview.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Root cause of the persistent black preview, fully isolated: ZAMPP1's nginx
serves the live .m3u8 fresh on every request (no-store works there), but
the PUBLIC reverse proxy (159.112.211.103 -> ZAMPP1) caches the static
.m3u8 by path with a multi-second TTL, ignoring both the origin's no-store
and query params. hls.js reloads the playlist ~every second, always landing
inside that TTL, so it sees the live playlist as never advancing
("live playlist MISSED" forever), never establishes the timeline, and never
loads a fragment -> readyState 0 (black). Proven: rapid reads via ZAMPP1
localhost advance (404->405); the same rapid reads via the public URL are
stuck; query-busting doesn't help (proxy caches by path).
Fix: serve the playlist through GET /api/v1/playout/channels/:id/hls/index.m3u8
instead of the static /media/live path. /api/ is not proxy-cached (the live
status poll already updates fine through it), so hls.js always gets the fresh
live edge. Segment (.ts) lines are rewritten to absolute /media/live/<id>/
URLs so they still load from the static path (immutable; caching them is
correct). ProgramMonitor points hls.js at the /api playlist and sends the
session cookie (xhrSetup withCredentials) since /api is auth-gated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Definitive root cause of the black preview, proven in-browser: the live
.m3u8 was served Cache-Control: no-cache, so the browser cached the
playlist and served a STALE copy to hls.js's reloads (cache:'default'
stuck at one MEDIA-SEQUENCE while cache:'reload' advanced). hls.js saw the
live playlist as never advancing -> "live playlist MISSED" forever ->
never established the timeline -> never loaded a fragment -> readyState 0
(black), even though the stream itself is clean and advancing server-side.
Fix: serve live HLS (/live and /media/live) with
"no-store, no-cache, must-revalidate" + Pragma no-cache so the browser
never caches the playlist and every reload fetches the fresh live edge.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The audio-strip fix made the stream decode cleanly server-side, but the
browser monitor was still black: with -c:v copy the re-mux passed through
CasparCG's erratic real-time keyframes (segments 0.6-2.8s) and irregular
PTS. hls.js can't build a live timeline from that — it logs
"sliding 0.00 / prev-sn na / MISSED" and never loads a fragment (verified
live in-browser: readyState stays 0). A standalone ffmpeg honours
-force_key_frames, so re-encode to libx264 with a forced 2s GOP + CFR
fps=30, scaled to 720p for a light confidence monitor. Every HLS segment
is now a clean, keyframe-aligned 2.0s chunk hls.js can sync to.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Growing-files mode became per-recorder (recorders.growing_enabled); the
global growing_enabled setting was removed. GET /assets/:id/live-path —
which the Premiere plugin calls to mount a still-growing master — still
required growing_enabled==='true', so Mount Live would 409 "Growing-files
mode is disabled" on any deploy where that stale key isn't set. Drop the
global gate: a status='live' asset already proves a growing recorder is
producing the file; only the editor-facing growing_smb_url is required.
Response contract is unchanged, so the plugin needs no update.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements docs/superpowers/specs/2026-05-31-storage-settings-growing-smb-design.md.
1. Storage warning banner at the top of Settings → Storage (set-once /
path-change-corrupts-data warning).
2. Growing-files SMB credentials + system CIFS mount (Approach A):
- settings.js: new global keys growing_smb_mount / growing_smb_username /
growing_smb_vers; growing_smb_password is write-only (GET returns only
growing_smb_password_exists; growing_smb_password_clear:true removes it).
- GrowingSettingsCard: SMB mount/username/password (masked, "saved" state) +
CIFS version fields.
- capture Dockerfile: add cifs-utils + util-linux.
- capture-manager: on growing start, mount //host/share at /growing using a
root-only credentials file (creds never on the command line); unmount on
stop; mount failure falls back to S3 streaming so a recording is never lost.
- recorders.js: pass GROWING_SMB_* env; don't host-bind /growing when a CIFS
mount is configured (an empty mountpoint is required).
3. Per-recorder growing mode (global toggle removed):
- Removed the global "capture writes to local SMB share first" checkbox; the
growing card is now SMB-infrastructure-only.
- recorders.js reads the per-recorder recorders.growing_enabled column
(already present from migration 014) instead of the global setting;
RECORDER_FIELDS += growing_enabled.
- New-recorder modal: "Growing-files mode" toggle.
- storage.js overview: "enabled" now means the SMB landing zone is configured
(mount source set), surfaced as smb_mount; health strip labels updated.
No DB migration required (recorders.growing_enabled exists; new settings are
key/value rows).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CasparCG 2.4.0's config parser aborts at startup (SIGABRT, exit 134) on
nearly every launch when the config contains XML comments / blank lines.
Proven empirically: identical image + comment-free config = 8/8 stable
starts; + commented config = 0/8 (all abort before logging init). Use
the compact comment-free form that the known-good image shipped.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
apt-get install ffmpeg pulls in ~80 transitive shared libs (libav*,
libx264, libdrm, libva...) that perturb CasparCG 2.4.0's headless
runtime linking and make it abort with SIGABRT (exit 134) on almost
every launch. Replace it with john van sickle's self-contained static
ffmpeg/ffprobe binaries in /usr/local/bin — the standalone CLI the HLS
re-muxer needs, with zero new shared libraries, keeping CasparCG's
environment identical to the known-good image.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 2.4.x server aborts at startup if its configured log-path isn't
writable/creatable. /opt/casparcg is a read-only-ish symlinked install
dir; the entrypoint already mkdirs /media/casparcg/{log,data}. Point the
config there to match (the working image used these paths).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>