Phase 0.2 of the NVENC All-Intra HEVC ingest plan.
node-agent/handleSidecarStart:
- Accept useGpu: true in the sidecar start body
- When useGpu: adds Runtime=nvidia, DeviceRequests=[gpu], and injects
NVIDIA_VISIBLE_DEVICES=all + NVIDIA_DRIVER_CAPABILITIES=video,compute,utility
into the container env. CPU-codec recorders are unaffected (useGpu defaults false).
mam-api/recorders (start endpoint):
- Derive useGpu from recorder.recording_codec — true for hevc_nvenc/h264_nvenc
- Pass useGpu to remote sidecar start body
- Apply same Runtime/DeviceRequests to the local Docker spawn path
capture/capture-manager:
- Update hevc_nvenc codec entry with all-intra flags:
-g 1 -bf 0 (every frame IDR, no B-frames — required for growing-file
edit-while-record), -rc vbr, -profile:v main10, pixFmt p010le (10-bit 4:2:0)
Next: validation gate (§8) — test MXF OP1a then fragmented MOV on one
DeckLink channel, mount in Premiere while recording.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Stuck-live fix: capture sidecar now finalises the pre-created live asset by id (new POST /assets/:id/finalize) instead of POSTing a new asset (409 collision); node-agent gives the sidecar a 180s stop grace so the S3 upload + callback complete; node-agent logs sidecar start/stop for diagnostics.
Live SDI monitor: HLS preview is now a 2nd output of the hires ffmpeg (single DeckLink read, split to ProRes/S3 + H.264/HLS); node-agent serves /live over HTTP; mam-api proxies GET /recorders/:id/live/* to the recorder node; web-ui HlsPreview loads from the proxied URL.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- capture/src/index.js: read MAM_API_TOKEN from env; include
Authorization: Bearer header in shutdown callback fetches to mam-api
(POST /assets and POST /assets/:id/mark-empty). Without this, mam-api
AUTH_ENABLED=true rejects the callback with 401, leaving assets stuck in live
- recorders.js: pass MAM_API_TOKEN=${CAPTURE_TOKEN} in sidecar env so the
capture container receives the token at boot
- api_tokens: inserted capture-sidecar token (unbound, prefix b3d3d3c4)
## capture service
- capture-manager.js: add 'deltacast' source_type to _buildInputArgs.
Uses 'deltacast://<index>' with ffmpeg deltacast demuxer when
/dev/deltacast<N> exists; falls back to lavfi testsrc2 + sine test card
(matching deltacast-sdi-recorder standalone app) when hardware absent.
- routes/capture.js: add GET /devices/deltacast endpoint (enumerates
/dev/deltacast* + DELTACAST_PORT_COUNT env fallback). Extend /probe to
handle source_type=deltacast.
## node-agent
- detectHardware(): add 'deltacast' array to capabilities payload.
Enumerates /dev/deltacast* nodes; falls back to DELTACAST_PORT_COUNT env.
Adds DELTACAST_MODEL env support. Logs dc= count in heartbeat line.
- sidecar /start: bind /dev/deltacast* device nodes into capture containers
when sourceType='deltacast'.
## mam-api
- cluster.js: add GET /cluster/devices/deltacast and
GET /cluster/devices/deltacast/signal endpoints — same shape as
blackmagic equivalents for UI parity.
- recorders.js /start: pass DELTACAST_PORT_COUNT env to capture container;
bind /dev/deltacast* device nodes on local spawn.
- migration 024: ALTER TYPE source_type ADD VALUE 'deltacast' (idempotent).
- schema.sql: add 'deltacast' to source_type ENUM for fresh installs.
## web-ui
- modal-new-recorder.jsx: add 'Deltacast' source type card; fetch
/cluster/devices/deltacast on selection; port picker with TEST CARD
badge when hardware absent; falls through to manual index entry if
no devices detected.
DeckLink Duo 2 does not support two simultaneous ffmpeg processes on the
same port. The second (proxy) process immediately gets 'Cannot Autodetect
input stream or No signal', producing an empty upload that could crash the
container before the hires upload completes.
Fix: remove the parallel proxy spawn for SDI entirely. proxyKey is now
always null for SDI recordings (same as SRT/RTMP). needsProxy=true is
already set when proxyKey is null, so the BullMQ worker generates the
proxy from the hires master after stop — same pattern that works for
network sources.
Also revert bad regex change: ffmpeg -sources decklink output on this
hardware uses hex-address format ('81:76669a80:00000000 [DeckLink Duo (1)]')
not bare indented names — original regex was correct.
- 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.
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
The previous patch_decklink.py mixed v14_2_1 versioned types (Fix 1 renamed the allocator class) with no-ops for SetVideoInputFrameMemoryAllocator + QueryInterface-around-GetBytes (Fixes 2 & 3). That inconsistency compiled but silently dropped every video frame: VideoInputFrameArrived saw _v14_2_1 allocator output but tried to read it via the SDK 16 unversioned IDeckLinkVideoBuffer path, and the SDK released the buffer before FFmpeg could consume it.
Bisected with the BMD-provided Capture sample at SDK 16 mode 5 (Hp29) which got frames cleanly, confirming the signal was fine and the bug was in FFmpegs decklink demuxer.
Fix: pull libavdevice/decklink_{enc,dec,common}{.cpp,.h} from upstream FFmpeg master (commits past 7.1 that fully rename every decklink interface to its _v14_2_1 versioned form) and apply that diff in reverse during build. Now build is internally consistent and frames flow.
Verified: SDI1 recorder on zampp2 hits 423 frames in 14s @ 29 fps, ProRes HQ at 91 Mbps.
Bug: yadif=mode=1 unconditionally doubled output framerate for SDI input. On 1080p29.97 progressive sources the encoder produced zero frames (time advanced, size stayed at 1KiB MOV header).
Fix: deint=1 makes yadif only process frames flagged as interlaced; progressive frames pass through at the source rate.
Dockerfile is now a two-stage build that compiles FFmpeg from source with --enable-decklink against the Blackmagic SDK 16.x headers in services/capture/sdk/ (operator-supplied, gitignored). build-with-decklink.sh + patch_decklink.py drive the build.
docker-compose.yml mounts /dev/shm, /run/dbus, /run/systemd into mam-api, capture, web-ui so the BMD runtime can talk to the host.
capture-manager.js wraps SDI sources with -vf yadif=mode=1 (deinterlace).
recorders.html defaults to SDI source type now that we have a working DeckLink path.
Adds a VIDEO_CODECS / AUDIO_CODECS / CONTAINER catalogue and a
buildEncodeArgs() that composes -c:v / -c:a / -b:v / -b:a / -r / -ac / -f
from the recorder's saved settings. Master and proxy each get their own
codec stack, both honour the container format chosen in the UI, and the
S3 keys now use the actual container extension instead of hardcoded mov/mp4.
While a recorder is running, the capture container tees an HLS
stream into /live/<assetId>/ alongside the ProRes master upload.
The asset row is pre-created at recorder start with status='live'
so the clip appears in the library immediately. /api/v1/assets/:id/stream
returns the HLS playlist URL until recording stops, then proxy.
* docker-compose: shared wild-dragon-live mount on api/capture/web-ui
* migration 001-add-live-status: idempotent ALTER TYPE for asset_status
* mam-api: runMigrations() on boot; recorders.js pre-creates live asset
+ passes ASSET_ID; assets.js POST upserts on existing live row instead
of inserting a duplicate, and stream route returns HLS for live assets
* capture: parallel HLS ffmpeg into /live/<assetId>/; ASSET_ID env
* web-ui: nginx serves /live/, preview.js loads hls.js, LIVE badge added
Two things that together stop bogus URLs from masquerading as a recording:
PROBE BUTTON in the New Recorder panel. Before you commit to record, hit Probe Source - the capture container runs ffprobe with a 10s timeout against the URL and returns the parsed streams. UI shows green Signal Detected with codec/resolution/fps/audio, or red No Signal Detected with the actual ffprobe error message. For SDI it lists DeckLink devices. Listener-mode sources cannot be probed standalone (would block waiting for a publisher) and the UI says so.
MAIN STATUS LABEL ON THE RECORDING CARD now mirrors the live signal instead of hardcoding Recording. So a recorder pointed at a dead URL goes Connecting... -> Connection error (red) instead of looking like everything is fine. When frames actually start arriving the label flips to Recording (blue) and the dot turns blue. If a previously-good stream drops the label switches to Signal lost (red).
API:
* capture: POST /capture/probe runs ffprobe and returns { ok, streams, format, error? }
* mam-api: POST /api/v1/recorders/probe proxies through to the capture sidecar with a 15s outer timeout
Three problems blocked the end-to-end flow:
1) Library always rendered empty because /assets returns {assets,total} but
index.html (and capture.html) assumed r.data was an array. Fixed in
api.js by unwrapping r.data.assets centrally; total is kept on r.total.
2) SRT/RTMP caller mode pulled audio only. ffmpeg opened the network input
before the H264 SPS arrived, marked the video stream as pix_fmt=none,
and silently dropped it from the stream map. Added -probesize 32M
-analyzeduration 10M -fflags +genpts and explicit -map 0✌️0?/0🅰️0? so
each track survives independently of when it appears.
3) Hitting Record gave no feedback about whether a stream was actually
arriving. capture-manager now parses ffmpeg progress lines (frame=...
fps=...) and tracks framesReceived, currentFps, lastFrameAt, lastError.
getStatus() returns a derived signal enum (connecting | receiving |
lost | error | stopped). The recorder controller gives each spawned
container a stable network alias `recorder-<id>` and the GET
/recorders/:id/status endpoint proxies the live capture status through.
recorders.html polls that every 2s and renders the badge under each
active card with the running frame/fps counter or the ffmpeg error.
Also:
* recorders.html: dropped the listener-mode UI entirely. All new recorders
are caller-mode (pull). The MAM is no longer offered as an RTMP/SRT
server. Legacy listener records still render but read-only.
- Accept source_type, source_url, listen, listen_port, stream_key
- Validate: SDI requires device; SRT/RTMP caller requires source_url
- Pass all params through to captureManager.start()
- On stop: if proxyKey is null (network source), include needsProxy flag
in MAM API registration so worker can generate proxy asynchronously