Compare commits

...

110 commits

Author SHA1 Message Date
Zac
1c068b470e fix(capture-manager): default deltacast framerate to 60000/1001 (1080p59.94) 2026-06-02 01:15:42 +00:00
Zac
e4154ea83a fix(node-agent): skip bridge spawn if FIFOs already exist (external bridge) 2026-06-02 01:04:15 +00:00
Zac Gaetano
cf928d1a46 Merge: remove Capture nav, add Playout warning 2026-06-02 00:27:26 +00:00
Zac Gaetano
f2f3a88308 Merge: deltacast multi-port bridge + UI fixes 2026-06-02 00:27:26 +00:00
Zac Gaetano
b697d356b2 fix(node-agent): inject per-port bridge format JSON into deltacast sidecar env
Capture bridge emits per-port format JSON on signal lock. Node-agent
now caches these by port and injects DELTACAST_VIDEO_SIZE, DELTACAST_FRAMERATE,
DELTACAST_INTERLACED into the sidecar env so capture-manager uses
actual signal dimensions instead of hardcoded 1920x1080/25fps defaults.
2026-06-02 00:27:18 +00:00
ebeaf01a67 tools: add na_patch.py for node-agent fmt injection 2026-06-01 20:27:03 -04:00
Claude
a61e385693 feat(deltacast): replace per-port bridges with shared multi-port daemon
The old architecture spawned one deltacast-capture per recorder port; each
called VHD_OpenBoardHandle, triggering a BufMngr.c:781 OOB fault in the
delta_x300 kernel driver whenever two opens raced.

Fix: a single deltacast-bridge daemon opens the board once, opens RX
streams for all requested ports concurrently, and writes each port's
video/audio to named FIFOs (/dev/shm/deltacast/video-<N>.fifo,
/dev/shm/deltacast/audio-<N>.fifo). Capture sidecars read from those
FIFOs directly — no board handle, no race, no flock.

Changes:
  services/capture/deltacast-bridge/main.c
    - Complete rewrite: --ports csv arg, board opened once, one
      video+audio thread pair per port, FIFO paths per port, format
      JSON emitted per port on signal lock, SIGTERM clean shutdown.
    - flock/serialize logic removed (no longer needed).
    - --port single-port compat alias retained.
  services/capture/deltacast-bridge/CMakeLists.txt
    - Rename target deltacast-capture -> deltacast-bridge.
    - POST_BUILD symlink deltacast-capture -> deltacast-bridge for compat.
  services/capture/src/capture-manager.js
    - deltacast _buildInputArgs: remove bridge spawn; wait up to 30s
      for FIFOs to exist (bridge may be starting); return rawvideo +
      s16le FIFO inputArgs. bridgeProcess=null.
    - audioMap: keyed on sourceType instead of bridgeProcess (both
      inputs are always present for deltacast).
    - Remove readFirstStderrLine helper (no longer needed).
    - Remove bridgeProcess.stdout.pipe / processes.bridge stop signal.
  services/node-agent/index.js
    - Add import spawn for bridge daemon management.
    - Add startDeltacastBridge / stopDeltacastBridge: host-process
      lifecycle for the shared bridge, ref-counted by sidecar count.
    - handleSidecarStart: on deltacast, increment counter + start bridge;
      decrement on container create/start failure.
    - handleSidecarStop: decrement counter; stop bridge when last sidecar.
    - _containerSourceType map tracks containerId->sourceType for stop.
    - Old acquireDcLock mutex retained but no longer called.
2026-06-02 00:21:52 +00:00
Zac
bf7189218a fix(web-ui): remove Capture nav item, add Playout testing warning
Remove 'Capture' from the Operations nav section in shell.jsx — users
configure recorders via the Recorders page; the Capture route/component
is left intact for any remaining references.

Also remove 'capture' from the ingest open-group list (it was listed
as an ingest child despite living in Operations, now moot).

Add a prominent amber warning banner at the top of the Playout page
body (screens-playout.jsx) to make clear the feature is in testing and
not ready for production use.

No cherry-pick from fix/library-and-signal-indicator — all commits on
that branch are already present on main.
2026-06-02 00:17:04 +00:00
d66d4bea80 tools: add sigcheck.c — per-port SDI signal scanner 2026-06-01 19:41:54 -04:00
cad1e52c38 fix(deltacast-bridge): reset signal-wait deadline AFTER acquiring flock
The signal timeout deadline was set at process start before waiting for
the flock. Bridges queued behind earlier ports waited minutes for the
lock, then found their 30s signal deadline had already expired before
they even opened the board, causing false "no signal" failures on ports
that have live signal.

Fix: move clock_gettime deadline initialisation to AFTER flock acquired
and board opened, so the full sig_timeout is always available for signal
detection regardless of queue wait time.
2026-06-01 19:32:01 -04:00
Zac Gaetano
6979f07307 fix(capture-manager): raise readFirstStderrLine timeout 35s->300s for flock queue
With 8 deltacast bridges serializing via flock (each holding the lock
for ~35s during signal wait + settle), the last bridge in queue waits
~280s before getting the lock. The 35s readFirstStderrLine timeout was
firing before those bridges could even open the board, causing them to
fail silently while the bridge was still queued. 300s (5min) covers
8 bridges * 35s each with margin.
2026-06-01 23:23:07 +00:00
Zac Gaetano
eba8e94887 fix(capture-manager): readFirstStderrLine skips non-JSON bridge log lines
The flock-based board serialization in deltacast-bridge emits [board] log
lines to stderr before the JSON format line. readFirstStderrLine was
failing on the first non-JSON line. Now loops over complete lines,
skips any not starting with {, and waits for the actual JSON.
2026-06-01 23:09:21 +00:00
a06b5ed304 fix(capture-manager): readFirstStderrLine skips non-JSON bridge log lines
The deltacast bridge now emits [board] log lines before the format JSON
(while waiting for flock). readFirstStderrLine was parsing the first line
only and failing with 'invalid JSON'. Now it accumulates all lines and
skips any that do not start with '{', continuing to wait for the JSON
format line. Error lines ({\"error\":...}) still reject immediately.
2026-06-01 19:07:15 -04:00
7d704d3af3 fix(deltacast-bridge): serialize VHD_OpenBoardHandle via flock to prevent BufMngr wedge
Concurrent VHD_OpenBoardHandle calls from multiple capture sidecars
trigger delta_x300 BufMngr.c:781 array-index-out-of-bounds, wedging all
RX channels until the module is reloaded. The node-agent stagger only
delays container start — the bridge binary starts ~2s later and can still
race. This fix acquires an exclusive flock on /dev/shm/deltacast/bridge.lock
before VHD_OpenBoardHandle and holds it until signal lock succeeds (then
adds a 4s settle before releasing so the board's buffer queues stabilize).
Lock is released on signal failure too so the next bridge is never
permanently blocked. All 8 channels can now start safely by serializing
through the same lock file mounted into every sidecar.
2026-06-01 18:54:00 -04:00
b324878db9 fix(node-agent): serialize deltacast sidecar opens to prevent BufMngr wedge
Simultaneous VHD_OpenBoardHandle calls from 8 sidecars trigger a kernel
array-index-out-of-bounds in delta_x300 BufMngr.c:781 that wedges all
RX channels. Serialize deltacast-only sidecar starts through a
promise-chain mutex with a configurable settle delay
(DELTACAST_START_STAGGER_MS, default 3500ms). All other source types
(SDI, SRT, RTMP) are unaffected — they bypass the mutex entirely.
2026-06-01 18:47:55 -04:00
Zac Gaetano
9809cdd13e fix(capture): lower GROWING_DEFAULT_BITRATE 50M→25M
25 Mbps is sufficient for XDCAM HD422 1080i/1080p at broadcast quality
and halves storage use. Operators can still override via recording_video_bitrate.
2026-06-01 22:42:07 +00:00
75f265534e fix(web-ui): default recorder bitrate 60→25 Mbps, HEVC preset 60→25 2026-06-01 18:41:36 -04:00
8b8a19c465 fix(node-agent): serialize deltacast sidecar opens to prevent BufMngr wedge
Simultaneous VHD_OpenBoardHandle calls from 8 sidecars trips a kernel
array-index-out-of-bounds in BufMngr.c:781 (delta_x300 v6.34.1). Fix:
a process-wide promise-chain mutex gates deltacast sidecar starts so only
one board open is in flight at a time, with a configurable settle delay
(DELTACAST_START_STAGGER_MS, default 3500ms) before releasing the lock.
SDI, SRT, RTMP and all other source types are unaffected.
2026-06-01 18:41:35 -04:00
9adcae0329 fix(capture): connect deltacast growing-master filtergraph (split output 1 unconnected)
The growing-master ffmpeg orchestrator declared split=2[vhi][vlo] but only
consumed [vlo] inside the `if (hlsDir)` block. For deltacast sources the
caller passed hlsDir=null (the ternary only matched sourceType==='sdi'), so
[vlo] was left unconnected → ffmpeg aborted with "Filter 'split' has output 1
(vlo) unconnected" / "Error binding filtergraph inputs/outputs" → 0 frames →
no HLS → "playback failed" on all deltacast previews.

Fix:
- Pass sdiHlsDir for deltacast as well as sdi (deltacast also produces the
  2nd-output HLS preview from the single SDI read).
- Make the orchestrator filter_complex conditional: split=2[vhi][vlo] when an
  HLS dir is present, split=1[vhi] (master only) otherwise, so no split output
  is ever orphaned regardless of source type.

Restores deltacast growing-master capture (master MXF + HLS preview). No poster
tap (the incomplete recorder-thumbnails poster on the deploy node added an
mjpeg output that destabilised the shared ffmpeg; tracked separately on the
feature/recorder-thumbnails branch).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 16:10:21 -04:00
63654ea0ed feat(deltacast): persist selected channel as source_config.port
The Deltacast picker's selected index is the capture channel on the single
board. Write it into source_config.port (in addition to device_index) so the
capture sidecar maps "pick channel N" to the bridge's --port N. device_index is
retained for backward-compatible display/fallback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 15:00:48 -04:00
0eac34529b feat(deltacast): wire source_config.port/board into capture start
Parse the recorder's SOURCE_CONFIG JSON in the bootstrap and pass the deltacast
capture channel (`port`) and optional `board` into captureManager.start(), so a
recorder can select which of the board's 8 channels to capture.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 14:58:58 -04:00
d6e515e1a8 fix(capture): map deltacast audio from input 1; per-recorder channel/port; fix bridge stdin pipe
- Audio map: the deltacast bridge delivers audio on a separate FIFO wired as
  ffmpeg input 1, so the finalized master + HLS preview (and the growing
  orchestrator) now map audio via `audioMap` (1🅰️0? for deltacast, 0🅰️0? for
  DeckLink SDI / network) instead of an unconditional 0🅰️0?. Without this the
  deltacast master/preview carried no audio.
- Channel/port: spawn the bridge with --device = board index (default 0) and
  --port = source_config.port (falling back to the device index), so a recorder
  can capture from any of the board's 8 channels. Adds `port`/`board` params to
  start() and _buildInputArgs().
- Bridge stdin: the finalized-master ffmpeg reads the bridge's raw video from
  pipe:0, so its stdin must be 'pipe' when a bridge is present (was 'ignore',
  which made hiresProcess.stdin null and threw "Cannot read properties of null
  (reading 'on')" at bridgeProcess.stdout.pipe(...)).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 14:57:53 -04:00
3d3c8c48de fix(deltacast): always open audio FIFO writer (silence fallback) to stop ffmpeg input deadlock
ffmpeg opens all inputs before processing; input 1 is the audio FIFO. The
bridge previously opened the FIFO writer only after VHD_OpenStreamHandle +
VHD_StartStream succeeded, returning early on failure / no embedded audio and
never opening the FIFO -> ffmpeg blocked forever on input 1 -> 0 fps and an
empty HLS preview. Now the FIFO writer is opened unconditionally and first,
and the audio thread feeds a continuous, wall-clock-paced s16le stereo stream
(real samples when available, otherwise silence). SIGPIPE is ignored so a
dying ffmpeg returns EPIPE instead of killing the bridge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 14:52:24 -04:00
b65ce5b0b7 fix(web-ui): recorder modal body scrolls (min-height:0) so codec/destination aren't clipped at zoom 2026-06-01 13:49:10 -04:00
323d482eab feat(deltacast): install 99-wild-dragon-deltacast.rules to create 8 /dev/deltacast symlinks 2026-06-01 13:34:07 -04:00
8f33cbfa86 feat(deltacast): udev rule to create 8 /dev/deltacast symlinks 2026-06-01 13:31:29 -04:00
12d76edc42 feat(deltacast): support 8 RX channels (ports 0-7) on DELTA-12G-elp-h 2026-06-01 13:31:06 -04:00
551377e4c9 fix(node-agent): mount /dev/shm/deltacast into capture sidecars so VideoMaster SDK detects the Deltacast board 2026-06-01 12:56:12 -04:00
b875376887 Merge remote-tracking branch 'wilddragon/main' 2026-06-01 09:22:02 -04:00
252aa713d4 fix(capture): drop dur-patch; rdd9 self-maintains growing Duration
On-node empirical testing of this bmx v1.6 build showed that raw2bmx's
rdd9 writer with --part already maintains a live, correct header Duration
as the file grows: ffprobe reads a growing duration mid-write (e.g. 2.04s
of a 10s clip while still recording), and the structural-metadata
Duration fields (tags 02020008 / 30020008) hold the real frame count
(0x33 = 51), not -1.

The dur-patch.py added in the previous commit searched the header for
Duration=-1 (0xFF*8) and found 0 fields on rdd9 ("[dur-patch] 0 Duration
fields"), so it was a no-op. Worse, opening the MXF r+b to patch it while
raw2bmx appends over CIFS is a concurrency hazard. Remove it entirely and
rely on raw2bmx's native growing Duration. rdd9 + --index-follows remains
the Premiere-recommended growing flavour (Sony XDCAM essence, index in the
essence partition).

Verified on-node (ffprobe/byte-probe). Live edit-while-record in Premiere
itself still requires user confirmation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 09:07:28 -04:00
068e2eaa87 fix(node-agent): NODE_NAME override to prevent cloned-VM hostname collision
The cluster heartbeat upserts cluster_nodes ON CONFLICT (hostname), so two
machines reporting the same os.hostname() clobber each other's row. A cloned
capture VM whose /etc/hostname was "zampp1" (same as the primary) caused its
4 DeckLink cards to land on the primary's row, then get overwritten by the
primary's cardless heartbeat — so the New Recorder modal showed "No SDI
devices auto-detected" despite healthy hardware.

- node-agent now reports process.env.NODE_NAME || os.hostname() as its cluster
  identity, so node identity is explicit and collision-proof.
- docker-compose.worker.yml exposes NODE_NAME to the container.
- onboard-node.sh always writes NODE_NAME to the node .env (defaults to the OS
  hostname) so future onboarding pins identity even on cloned images.

Live remediation already applied to the zampp2 capture node: compose hostname
pinned to zampp2 and its node token rebound to zampp2; DB now reports bmd=4
for zampp2.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 09:00:34 -04:00
e3be8745d3 fix(capture): restore frame counting + growing MXF for Premiere
The previous sed/python in-place edits on the node broke capture: the
hires stderr parser was written with literal 0x08 BACKSPACE bytes instead
of regex word boundaries, so it never matched ffmpeg output.
framesReceived stayed 0, the shutdown handler saw "no frames" and marked
every asset as an error even though video was captured. The ffmpeg base
args had also been changed to -progress pipe:2, whose key=value output
puts frame= and fps= on separate lines and does not match a combined
regex.

Fixes:
- Parser: single robust regex matching ffmpeg's classic -stats line
  (frame= and fps= together). No backspace bytes, no word boundaries.
- ffmpeg base args back to -stats (drop -progress pipe:2).

Growing-file (Premiere edit-while-record), per bmx thread 87ac5750 and
Drastic/Softron edit-while-ingest docs:
- raw2bmx clip type op1a -> rdd9 (Sony XDCAM / RDD-9, the flavour Premiere
  reads while growing) with --index-follows so the IndexTableSegment is
  written in the same partition as the essence it indexes (lets a reader
  re-scanning body partitions seek toward the record head). NOT --avid-gf
  (Avid OP-Atom, Media-Composer-only, needs a companion AAF).
- dur-patch.py: overwrite header Duration=-1 to 0 immediately at
  clip-open (Premiere rejects -1 on import), then track the live frame
  count every 3s from the last body partition IndexTableSegment. Shipped
  as services/capture/dur-patch.py (/app/dur-patch.py in the image).

Deployed to wild-dragon-capture:latest on zampp2 via overlay build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 09:00:23 -04:00
7d408035ac fix(capture): fix VHD_CORE_SP_BUFFERQUEUE_DEPTH constant name for SDK 6.34 2026-06-01 08:59:06 -04:00
884c8829a0 merge: integrate remote changes + deltacast bridge capture
Merges remote main (raw2bmx growing-file, playout, auth) with local
Deltacast SDI capture implementation (bridge binary, _buildInputArgs,
start/stop lifecycle).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 08:41:05 -04:00
962c7c8f20 fix(capture): log SDK errors in video loop, warn on unsupported port index 2026-06-01 08:00:55 -04:00
b46abc9b1a fix(capture): shell injection, stale FIFO, stderr listener, bridge exit handler 2026-06-01 08:00:06 -04:00
adfbeac217 feat(capture): accept deltacast as valid source_type in /start handler 2026-06-01 07:56:43 -04:00
d4924b044f feat(capture): wire bridge process lifecycle into start/stop for deltacast 2026-06-01 07:55:34 -04:00
4da7bf8b41 feat(capture): replace deltacast _buildInputArgs stub with real bridge spawn 2026-06-01 07:54:17 -04:00
6fec41aaf9 feat(capture): add readFirstStderrLine helper for deltacast bridge handshake 2026-06-01 07:53:12 -04:00
de6e44b991 build(capture): add Deltacast SDK extraction and bridge build stages to Dockerfile
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 07:52:03 -04:00
c7e07df515 fix(capture): handle partial writes in audio_thread FIFO write loop 2026-06-01 07:51:10 -04:00
67c071a0ee feat(capture): add deltacast-capture bridge binary source 2026-06-01 07:50:07 -04:00
3529590160 build(capture): add CMakeLists for deltacast-capture bridge binary 2026-06-01 07:47:32 -04:00
96f4f2dd3b docs: Deltacast SDI capture implementation plan
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 07:45:58 -04:00
1750298bb8 revert(capture): remove --growing-file flag (not valid in bmx v1.6)
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>
2026-06-01 07:37:27 -04:00
298cb18914 docs: Deltacast SDI capture design spec (bridge approach)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 07:34:09 -04:00
e8a1f564b0 fix(capture): add --growing-file flag to raw2bmx command
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>
2026-06-01 07:31:53 -04:00
bda33fedca fix(web-ui): sync route state with URL hash on mount + hashchange
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>
2026-06-01 11:10:19 +00:00
Zac Gaetano
66ff7065ec Constrain playout preview to 960px for 1080p screens
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>
2026-06-01 11:09:54 +00:00
c1512e29c5 feat(capture): true growing MXF via bmx/raw2bmx (Premiere edit-while-record)
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>
2026-05-31 23:23:46 -04:00
06e480f2b4 Add NODE_AGENT_TOKEN to .env.example
node-agent needs bearer token to auth with mam-api /driver/* endpoints.
Missing token causes 401 on driver status checks.

Generate with: openssl rand -hex 32
2026-05-31 23:11:40 -04:00
Zac
9bcbac558c Add multi-select to library page
- Selection mode toggle in toolbar
- Checkboxes on cards (grid) and rows (list)
- Bulk actions: move to bin, delete
- Select all / clear selection controls

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-06-01 03:01:19 +00:00
2cd20a0e72 fix(growing): don't promote a growing file while its recorder is still recording
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>
2026-05-31 22:26:07 -04:00
35165e28a8 fix(gui): modal reflects MXF XDCAM growing format (was stale MPEG-TS text)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 22:14:52 -04:00
32a2d0329e fix(growing+gui): growing file = MXF XDCAM HD422 (Premiere-growable) + GUI fixes
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>
2026-05-31 22:13:01 -04:00
b92a5bc7f7 fix(web-ui): playout fmtDuration no longer clobbers global asset duration formatter
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>
2026-05-31 22:02:46 -04:00
7b878d48c9 fix(capture): growing TS is 8-bit 4:2:0 (Premiere-importable) + honor bitrate
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>
2026-05-31 21:56:58 -04:00
64bbb221f7 fix(api): parse Postgres bigint (int8) as Number, not string
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>
2026-05-31 21:47:45 -04:00
Zac
ef329399f1 feat: v2.2.3 plugin + duration fix
- 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>
2026-06-01 01:10:55 +00:00
984a73e8ec feat(playout): redesigned MCR screen + SCTE-35 end-to-end
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>
2026-05-31 19:58:02 -04:00
8a958046ef fix(growing-files): MPEG-TS growing master + promotion-worker share mount
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>
2026-05-31 19:41:28 -04:00
08499b93b2 feat(gpu+capture): nvenc HLS preview, source-backend abstraction, GPU affinity+telemetry
#164 HLS preview uses h264_nvenc (forced-IDR, GOP=segment) when the sidecar
has the GPU, else keeps libx264 fallback.
#168 source-backend abstraction in capture-manager (blackmagic implemented as
a behavior-preserving refactor; deltacast/aja stubbed pending hardware).
#167 per-recorder gpu_uuid (migration 032) plumbed mam-api->agent->
NVIDIA_VISIBLE_DEVICES (defaults to 'all').
#166 node-agent reports encoder util + NVENC session count per GPU; Cluster
screen renders per-GPU GPU/ENC util, VRAM, sessions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 18:38:56 -04:00
ca1eec0600 fix/feat: recorder finalize-grace + codec validation, cluster mem/version, library download
#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>
2026-05-31 18:34:36 -04:00
794b9d9929 fix(capture): growing file is MXF OP1a (DNxHR HQ) so Premiere can open it
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>
2026-05-31 18:31:07 -04:00
1d642bd437 feat(web-ui): in-page delete confirm modal + WDL home footer
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>
2026-05-31 18:31:07 -04:00
fffff1c016 feat(cluster): install capture-card drivers/SDKs from the admin screen
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>
2026-05-31 18:14:59 -04:00
549ca6c73f fix(capture): finalized MOV master is non-fragmented so Premiere can open it
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>
2026-05-31 18:14:59 -04:00
ea615c8c76 chore(capture): commit Blackmagic DeckLink SDK 16.0 Linux headers
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>
2026-05-31 18:07:12 -04:00
f2d5f5aa16 feat(cluster): auto-assign node profiles from detected hardware
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>
2026-05-31 18:02:29 -04:00
d908c0c056 fix(playout+capture+monitors): preview recovery, SMB UNC, playout monitors
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>
2026-05-31 17:56:45 -04:00
b597ffd58e Merge redesign/panel-icon-rail into main (superseded)
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.
2026-05-31 17:48:44 -04:00
be8fd691a5 Merge fix/srt-rtmp-thumbnail into main (superseded)
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.
2026-05-31 17:47:52 -04:00
dba3435e60 Merge feat/all-intra-hevc-ingest into main
Rebuild capture ffmpeg with nv-codec-headers + NVENC/CUVID for All-Intra HEVC.

# Conflicts:
#	services/capture/Dockerfile
2026-05-31 17:46:59 -04:00
43011bd794 Merge feat/playout-mcr into main
Playout/MCR, as-run log, redesigned dashboard, capture CIFS/growing-files,
SDI settings, cluster Add Node wizard, homepage refresh.

# Conflicts:
#	services/mam-api/src/routes/cluster.js
#	services/mam-api/src/routes/playout.js
#	services/mam-api/src/scheduler.js
#	services/playout/Dockerfile
#	services/playout/entrypoint.sh
#	services/web-ui/public/screens-home.jsx
2026-05-31 17:46:12 -04:00
43656a5e88 shell: youtube nav icon: download → import
download is used by the Downloads section tile on the home screen.
YouTube ingest gets the new import icon (arrow entering box) instead.
2026-05-31 10:52:10 -04:00
68461af990 icons: add import icon (arrow entering box)
Distinct from download (vertical arrow+line). Used for YouTube ingest
to avoid sharing a glyph with the Downloads section tile.
2026-05-31 10:50:43 -04:00
8bc460025d screens-home: fix launcher tile icons
- Dashboard tile: home → layout (matches sidebar nav icon)
- Playout tile: monitor → signal (matches sidebar nav fix)
2026-05-31 00:19:34 -04:00
3578c7b4e9 fix(playout): Privileged only for decklink (SRT/NDI/RTMP/HLS crashed when GPU exposed without driver) 2026-05-30 18:59:27 -04:00
cddcc9a29e fix(mam-api): selfHeartbeat writes last_seen_at so primary node isn't stale-failover-killed 2026-05-30 18:57:20 -04:00
0e844c0fc3 fix(scheduler): use updated_at as grace anchor when last_heartbeat_at NULL
Without this, a freshly-spawned channel with NULL last_heartbeat_at was
instantly failover-killed by the playoutHealthTick because `0` was used as
the lastSeen timestamp, making ageMs huge on the very first tick.
2026-05-30 17:32:15 -04:00
551af09dc7 fix(playout): install libnss3 so CEF can init (NSS -8023 was killing the channel ~30s in) 2026-05-30 17:16:54 -04:00
4d6a999665 fix(playout): pre-create NSS dir + CEF cache so CEF/HTML producer doesn't SIGABRT 2026-05-30 17:14:07 -04:00
f971d57bb9 fix(playout): use unzip not python zipfile (preserves exec bits) 2026-05-30 17:00:25 -04:00
7ab70948a0 fix(playout): entrypoint handles 2.4.x bin/casparcg layout + LD_LIBRARY_PATH for bundled libs 2026-05-30 16:50:04 -04:00
13bbd4216e fix(playout): correct 2.4.0 zip layout — binary is at casparcg_server/bin/casparcg 2026-05-30 16:49:48 -04:00
fcd8e8dd2e fix(playout): entrypoint finds binary in /opt/casparcg for 2.4.x tarball layout 2026-05-30 16:44:23 -04:00
67ac007706 fix(playout): downgrade CasparCG to 2.4.0 ubuntu22 zip (2.5 requires AVX2, ZAMPP has AVX only) 2026-05-30 16:44:07 -04:00
b4f2fb12ff fix(mam-api): heartbeat writes last_seen_at so playout failover sees healthy nodes 2026-05-30 16:32:11 -04:00
aa7f836493 fix(playout): strip XML comments from casparcg.config (2.5 rejects them) 2026-05-30 16:30:54 -04:00
c2409bd037 fix(mam-api): add last_seen_at to cluster_nodes for playout failover
Playout failover queries cluster_nodes.last_seen_at to find healthy nodes
for channel re-placement. Column missing from original cluster schema.

Migration 031 adds column + backfills existing nodes to NOW().

Fixes scheduler error: column "last_seen_at" does not exist

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-05-30 13:39:06 -04:00
42064acefa shell: fix nav icon conflicts
- schedule: jobs → clock (was sharing hamburger icon with Jobs)
- playout: monitor → signal (was sharing TV icon with Monitors)
2026-05-30 13:30:42 -04:00
2e2b091653 icons: fix 4 icon issues found in audit
- jobs: replace hamburger (nav menu) with bulleted list (task queue)
- grid: add rx="1" to match library icon (consistency)
- hdd: replace circle+dot (vinyl) with cylinder (storage)
- proxy: replace upload-arrow with sliders (transcode/transform)
2026-05-30 13:29:18 -04:00
Zac
c502d4a16f feat(web-ui): update home tagline + add "Let's create" motto
Tagline "Self-hosted broadcast media-asset management" ->
"Media Asset Management & Production Platform"; add italic accent motto
"Let's create" below it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 16:04:28 +00:00
Zac
9d098e9778 feat(auth-ui): interactive permissions matrix, admin 2FA reset, Downloads button
Backend (routes/users.js):
- GET / now returns totp_enabled so the UI can show 2FA status
- GET /:id/access — admin-only effective per-project access (MAX over direct +
  group grants), labels via=direct|group:<name>; admins report all/edit
- POST /:id/totp/disable — admin clears a locked-out user's 2FA without their
  password (self-service disable still requires it); dev user blocked
- role validated against {admin,editor,viewer} on create + PATCH (was unchecked)

Frontend:
- Users>Policies tab: static prose replaced with interactive per-user matrix —
  inline role select, 2FA badge, Reset-2FA action, lazy per-user access expander
- Home "Premiere panel" tile -> "Downloads"; modal renamed, adds Teams ISO row
  (disabled "coming soon" until the .exe is supplied); UXP .ccx link unchanged
- data.jsx: window.TEAMS_ISO placeholder ({available:false})

Not runtime-tested in browser yet. Teams ISO .exe still pending from user.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 15:59:27 +00:00
Zac
02631f7b96 fix(playout): locate CasparCG 2.5 binary at /usr/bin/casparcg-server-2.5
First 2.5 build got past the deb install but the binary-discovery step
produced an empty $BIN (test -n failed): the 2.5 deb names its executable
casparcg-server-2.5, which the old case pattern (*/casparcg, */CasparCG
Server) didn't match. Broaden the match to /usr/bin/*casparcg*server*, fall
back to the known /usr/bin/casparcg-server-2.5, symlink it to
/usr/local/bin/casparcg, and make /opt/casparcg a real dir for our config
(no longer symlinked onto /usr/bin). Entrypoint launches `casparcg <config>`
from PATH instead of ./casparcg in a cwd.

Still NOT runtime-validated: 2.5 may reject the 2.3-era casparcg.config
schema (a bad config shows up as "Configuration file --version was not
found"); the deb ships a reference config at
/usr/share/casparcg-server-2.5/casparcg.config to diff against at smoke time.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 15:34:02 +00:00
Zac
9436434599 fix(playout): build CasparCG 2.5.0 from .deb (2.3.3 tarball was a dead URL)
The image never built: CASPAR_URL pointed at a v2.3.3-stable Linux tarball
that CasparCG never published (2.3.x is Windows-only; Linux builds start at
2.4.0, and 2.4.1+ ship only as .deb). Rewrite to install the 2.5.0 noble
server + CEF debs on an ubuntu:24.04 base (Node 20 via nodesource), letting
apt resolve the GL/ffmpeg/openal runtime deps. Binary install dir is
discovered from the deb file list and symlinked to /opt/casparcg so the
entrypoint + config still run from there. Move CasparCG log/data dirs to
/media (writable mount) since the install dir may be read-only.

NOT runtime-validated: the 2.5 casparcg.config schema and the AMCP consumer
syntax (ADD <ch> STREAM/FILE) were authored against 2.3 and must be smoke-
tested against 2.5 before a channel start can be trusted.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 15:25:31 +00:00
Zac
f837e57969 feat(web-ui): add Playout tile to home screen
Fetches /playout/channels separately and degrades silently when the
endpoint or schema is absent.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 14:59:59 +00:00
68c8f47c8f feat(capture): rebuild ffmpeg w/ nv-codec-headers + NVENC/CUVID for All-Intra HEVC
The custom FFmpeg 7.1 in this image previously had NO nvenc — only
prores_ks/dnxhd/libx264/libx265. The All-Intra HEVC ingest plan
(docs/design/2026-05-29-all-intra-hevc-ingest.md §4.1) needs
hevc_nvenc / h264_nvenc to offload the per-signal encode from CPU
to the L4, so we rebuild ffmpeg's configure stage with:

  --enable-ffnvcodec --enable-nvenc --enable-cuvid

nv-codec-headers (pinned n12.1.14.0) is cloned + installed before
configure so ffmpeg picks up the ffnvcodec pkg-config. Full CUDA
toolkit is omitted — only needed for GPU filters like yadif_cuda.

Adds a sanity-check RUN that fails the build if hevc_nvenc/h264_nvenc
don't appear in 'ffmpeg -encoders' so a broken NVENC build can't
silently ship. Creates /live and /growing as empty dirs since the
node-agent binds them at sidecar start.
2026-05-29 00:51:56 -04:00
Zac Gaetano
17bf086ef2 feat(web-ui): publish UXP panel v2.2.2 .ccx and repoint download buttons
Package the redesigned UXP panel as dragonflight-mam-2.2.2.ccx (plain zip
of manifest + index.html + styles.css + src/*, the standard UXP format that
double-click-installs on Windows and Mac via Creative Cloud).

- Add the .ccx to web-ui/public/downloads (served by nginx at /downloads).
- data.jsx: new v2.2.2 entry as latest (ccx field); old CEP releases kept.
- Editor screen: single "Download Panel (.ccx)" button.
- Settings -> Capture SDKs: .ccx download + updated UXP install copy;
  per-release buttons fall back to ZXP/Win for the old CEP entries.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 00:36:38 -04:00
Zac Gaetano
dac5213354 fix(uxp-panel): Export chooser is a full-panel screen, not a clipped popup
The floating popup anchored to the rail button clipped at the panel edge
(the bottom option was cut off). Replace it with a full-panel screen that
takes over the whole panel: a header with close, and two large option
rows (Conform Timeline -> MAM, Local Export) with icon + description.
Conform still routes to the codec-picker panel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-28 23:57:54 -04:00
Zac Gaetano
3f203f326e feat(uxp-panel): single Export menu, Upload to MAM, Local Export, auto-upload
UI consolidation:
- One Export entry (rail) opens a popup menu: Conform Timeline -> MAM and
  Local Export. Retires the standalone Export & Conform / Fetch & Relink
  dock buttons and the plain "Push Timeline" flow.
- Remove Import All.
- New "Upload to MAM" dock button.

Upload: reads the highlighted project-panel item(s) via the premierepro
API (best-effort, guarded) and falls back to a native file picker. Pushes
via /upload/simple (small) or chunked init/part/complete (large).

Local Export: batch-trim the timeline's hi-res clips server-side (FFMPEG),
poll trim-status, download each temp-segment-url, relink in Premiere.
Relink keys on source media path (last-wins for multi-use sources).

Conform + Local Export auto-upload any timeline sources not yet in the MAM
before proceeding (renders from the hi-res original, available post-upload).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-28 23:48:30 -04:00
Zac Gaetano
7e9f1277d4 fix(uxp-panel): rebuild Export & Conform panel - declutter + fix overlap
The preset cards overlapped the codec/quality dropdowns because the
2-column .preset-grid used flex-wrap, and UXP miscomputes wrapped-flex
container height (collapses to ~0), so the fields below rendered on top.

Rethink:
- Presets are now a single-column list (title left, spec right); no
  flex-wrap, so nothing collapses or overlaps.
- Progressive disclosure: the Codec/Quality/Resolution/Audio dropdowns
  are wrapped in #conform-custom and hidden unless the Custom preset is
  chosen. Default view is just Target project + 4 format rows.

The preset->select value contract is unchanged; presets still populate
the (now hidden) selects that Start Conform reads.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-28 23:13:15 -04:00
Zac Gaetano
9d8adbbbc1 feat(uxp-panel): fix hover tooltips + add compact list view toggle
Tooltips: the floating .tip-bubble used position:fixed, which UXP's
engine does not render reliably, so tooltips never appeared. Switch to
position:absolute (body never scrolls, so viewport coords still map),
add scroll-offset compensation, and guard against UXP returning 0 for
window.innerWidth/innerHeight.

List view: add a toolbar toggle (grid vs compact list). List view shows
a small 50x28 thumbnail with name/meta and an inline status chip per
row, fitting many more clips on screen. Defaults to list, persists via
localStorage when the host allows it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-28 23:05:52 -04:00
Zac Gaetano
3430ef823e fix(uxp-panel): icon controls as <div role=button> so UXP renders them
ROOT CAUSE: UXP renders native <button> chrome that ignores CSS
`background` and does not draw <svg>-only button content. The original
panel "worked" only because its buttons had TEXT (native buttons render
their text label); the redesign stripped text out, leaving empty grey
pills. Non-<button> elements (the <span> status chips, the <label>
search field + magnifier) render custom backgrounds and SVG children
correctly in this exact UXP -- proof divs are the right vehicle.

FIX: convert the 12 rail/dock/menu icon controls from <button> to
<div role="button" tabindex="0">. Divs have no native `disabled`, so
main.js installs a `disabled` accessor that reflects to a [disabled]
attribute; CSS keys disabled styling off [disabled]. Reverted the
non-working ::before cover back to direct backgrounds (divs honor them).
Text buttons (Connect, slide-panel actions, glyph close) stay <button>.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-28 22:55:45 -04:00
Zac Gaetano
08a0fb1b60 fix(uxp-panel): use ::before pseudo-element to cover native button chrome
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>
2026-05-28 21:09:09 -04:00
Zac Gaetano
dd438b597a fix(uxp-panel): render icons as filled SVGs so UXP draws them
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>
2026-05-28 20:44:50 -04:00
Zac Gaetano
8746d71af1 fix(premiere-panel): reset native button chrome on icon buttons
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>
2026-05-28 20:32:30 -04:00
Zac Gaetano
6a161c7133 feat(premiere-panel): icon-rail redesign with hover tooltips
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>
2026-05-28 17:57:26 -04:00
Zac
79369c378a fix: SRT/RTMP ingest + thumbnail crashes
Recorder model was creating capture containers but ffmpeg never spawned
inside them, so SRT/RTMP listeners bound the host port without ingesting
anything. Thumbnail extraction was also crashing on yuv444p sources,
leaving uploaded assets stuck at status=processing forever.

* capture/src/index.js: read RECORDER_ID/SOURCE_TYPE/LISTEN/LISTEN_PORT/
  STREAM_KEY/SOURCE_URL from env on startup and call captureManager.start()
  immediately. SIGTERM handler now flushes ffmpeg + S3 upload and POSTs the
  asset to mam-api before exiting.
* worker/ffmpeg/executor.js: force -pix_fmt yuv420p on proxy transcode and
  -pix_fmt yuvj420p on thumbnail extraction so mjpeg encoder accepts the
  input regardless of source pixel format.
* mam-api/routes/assets.js: when capture posts proxyKey=null but hiresKey
  is set (SRT/RTMP case), enqueue a proxy job from the hires so the asset
  ends up with a browser-playable proxy + thumbnail instead of stuck-ready.
* mam-api/routes/recorders.js: accept UI field aliases (codec/resolution/
  proxy_config), clean up unstarted containers on port collision, bump the
  docker stop timeout to 5min so long uploads can flush.
* web-ui/recorders.html: change default ports from 1935/9000 to 41936/49001
  to avoid common collisions with other RTMP/SRT services.
2026-05-17 07:01:54 -04:00
112 changed files with 13603 additions and 961 deletions

View file

@ -22,6 +22,11 @@ SESSION_SECRET=changeme
# MAM API Configuration # MAM API Configuration
MAM_API_URL=http://mam-api:3000 MAM_API_URL=http://mam-api:3000
# Node Agent Authentication
# Bearer token for node-agent to authenticate with mam-api /driver/* endpoints.
# Generate with: openssl rand -hex 32
NODE_AGENT_TOKEN=changeme
# Auth — default to ON in production. Setting to 'false' is a dev-only escape # Auth — default to ON in production. Setting to 'false' is a dev-only escape
# hatch that disables all auth checks and attaches a synthetic 'dev' user to # hatch that disables all auth checks and attaches a synthetic 'dev' user to
# every request. Never run with AUTH_ENABLED=false on a network you don't control. # every request. Never run with AUTH_ENABLED=false on a network you don't control.

4
.gitignore vendored
View file

@ -27,8 +27,8 @@ services/editor/**/node_modules
services/editor/**/dist services/editor/**/dist
services/editor/.pnpm-store services/editor/.pnpm-store
# Blackmagic DeckLink SDK + runtime libs (operator-supplied; see services/capture/build-with-decklink.sh) # Blackmagic DeckLink SDK headers are now committed (private/internal repo) under services/capture/sdk/.
services/capture/sdk/ # Runtime .so libs (libDeckLinkAPI.so) come from the DesktopVideo driver install and are not committed.
services/capture/lib/ services/capture/lib/
# Editor backups # Editor backups

325
deploy/install-driver.sh Normal file
View file

@ -0,0 +1,325 @@
#!/usr/bin/env bash
# ============================================================================
# install-driver.sh <vendor>
# ----------------------------------------------------------------------------
# Idempotent HOST installer for capture-card runtime drivers / SDKs.
#
# Runs ON the cluster node's HOST kernel. The node-agent invokes it inside a
# one-shot PRIVILEGED ubuntu container that bind-mounts this repo plus the host
# paths needed to affect the host kernel (/lib/modules, /usr/src, /boot, /dev,
# and the host apt/dpkg via the mounted root). dkms / modprobe / ldconfig
# therefore operate against the running host kernel.
#
# Reads the proprietary vendor file(s) from sdk/<vendor>/ (in this repo).
# NO binaries are committed — if the expected file is missing the script exits
# non-zero with a clear message telling the operator what to drop in.
#
# Vendors (ALLOWLIST — nothing else is accepted):
# blackmagic Desktop Video .deb (DKMS kernel module)
# aja NTV2 driver source/SDK (built kernel module)
# deltacast VideoMaster installer (kernel module)
# ndi redistributable runtime libs (user-space only, no module)
#
# Exit codes:
# 0 installed (or already present / up to date)
# 2 bad usage / unknown vendor
# 3 expected vendor file missing in sdk/<vendor>/
# 4 missing kernel headers (cannot build DKMS / module)
# 5 build / install / module-load failure
#
# `bash -n` must pass. set -euo pipefail with `|| true` guarding every probe.
# ============================================================================
set -euo pipefail
# ---------------------------------------------------------------------------
# Resolve our own repo dir (deploy/ -> repo root), regardless of CWD.
# ---------------------------------------------------------------------------
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
REPO_DIR="$(cd "$SCRIPT_DIR/.." >/dev/null 2>&1 && pwd)"
SDK_ROOT="$REPO_DIR/sdk"
VENDOR="${1:-}"
KVER="$(uname -r 2>/dev/null || echo unknown)"
REBOOT_REQUIRED=0
log() { echo "[install-driver] $*"; }
warn() { echo "[install-driver] WARN: $*" >&2; }
die() { echo "[install-driver] ERROR: $*" >&2; exit "${2:-5}"; }
usage() {
echo "Usage: install-driver.sh <blackmagic|aja|deltacast|ndi>" >&2
exit 2
}
[ -n "$VENDOR" ] || usage
case "$VENDOR" in
blackmagic|aja|deltacast|ndi) : ;;
*) echo "[install-driver] ERROR: unknown vendor '$VENDOR' (allowed: blackmagic aja deltacast ndi)" >&2; exit 2 ;;
esac
VENDOR_DIR="$SDK_ROOT/$VENDOR"
log "vendor=$VENDOR kernel=$KVER repo=$REPO_DIR"
log "reading vendor files from $VENDOR_DIR"
[ -d "$VENDOR_DIR" ] || die "vendor dir $VENDOR_DIR does not exist (repo not mounted?)" 3
# Pick the newest file matching a glob; echo its path or empty.
newest_match() {
# shellcheck disable=SC2012
ls -1t $1 2>/dev/null | head -n1 || true
}
ensure_headers() {
if [ -d "/lib/modules/$KVER/build" ] || dpkg -s "linux-headers-$KVER" >/dev/null 2>&1; then
log "kernel headers for $KVER present"
return 0
fi
log "installing linux-headers-$KVER ..."
apt-get update -y >/dev/null 2>&1 || true
if ! apt-get install -y "linux-headers-$KVER" >/dev/null 2>&1; then
# Fall back to the generic meta-package; still may not match a custom kernel.
apt-get install -y linux-headers-generic >/dev/null 2>&1 || true
fi
if [ -d "/lib/modules/$KVER/build" ] || dpkg -s "linux-headers-$KVER" >/dev/null 2>&1; then
log "kernel headers ready"
return 0
fi
die "kernel headers for $KVER unavailable — cannot build module. Install linux-headers-$KVER on the host." 4
}
# ===========================================================================
# blackmagic — Desktop Video .deb (DKMS) + modprobe + restart desktopvideo
# ===========================================================================
install_blackmagic() {
# Detect existing state first (idempotent skip).
if lsmod 2>/dev/null | grep -q '^blackmagic' && [ -e /dev/blackmagic ]; then
log "blackmagic module loaded and /dev/blackmagic present — already installed"
if command -v dkms >/dev/null 2>&1; then dkms status 2>/dev/null | grep -i blackmagic || true; fi
return 0
fi
local deb
deb="$(newest_match "$VENDOR_DIR/desktopvideo_*_amd64.deb")"
[ -n "$deb" ] && [ -f "$deb" ] || die \
"no desktopvideo_*_amd64.deb in $VENDOR_DIR — download Desktop Video for Ubuntu 22.04 and drop the .deb there (see sdk/blackmagic/README.md)." 3
log "using package: $(basename "$deb")"
ensure_headers
log "apt-get install (DKMS build) ..."
apt-get update -y >/dev/null 2>&1 || true
apt-get install -y "$deb" || die "apt-get install of $(basename "$deb") failed (DKMS build?)" 5
log "depmod + modprobe blackmagic ..."
depmod -a "$KVER" 2>/dev/null || true
if ! modprobe blackmagic 2>/dev/null; then
warn "modprobe blackmagic failed — a reboot may be required for the DKMS module to bind"
REBOOT_REQUIRED=1
fi
# Restart the DesktopVideoHelper daemon if present.
if command -v systemctl >/dev/null 2>&1; then
systemctl restart desktopvideo 2>/dev/null \
|| systemctl restart DesktopVideoHelper 2>/dev/null || true
fi
if lsmod 2>/dev/null | grep -q '^blackmagic' || [ -e /dev/blackmagic ]; then
log "blackmagic installed (module loaded / device present)"
else
warn "blackmagic installed but module not yet loaded — reboot required"
REBOOT_REQUIRED=1
fi
}
# ===========================================================================
# aja — build ntv2 kernel module from source archive + modprobe
# ===========================================================================
install_aja() {
if lsmod 2>/dev/null | grep -q 'ajantv2'; then
log "ajantv2 module already loaded — already installed"
return 0
fi
local src
src="$(newest_match "$VENDOR_DIR/ntv2sdk*.zip")"
[ -n "$src" ] || src="$(newest_match "$VENDOR_DIR/libajantv2*.tar.gz")"
[ -n "$src" ] && [ -f "$src" ] || die \
"no ntv2sdk*.zip / libajantv2*.tar.gz in $VENDOR_DIR — download the AJA NTV2 Linux SDK and drop it there (see sdk/aja/README.md)." 3
log "using source: $(basename "$src")"
ensure_headers
apt-get install -y build-essential unzip >/dev/null 2>&1 || true
local work; work="$(mktemp -d)"
log "extracting into $work ..."
case "$src" in
*.zip) unzip -q -o "$src" -d "$work" || die "failed to unzip $(basename "$src")" 5 ;;
*.tar.gz) tar -xzf "$src" -C "$work" || die "failed to untar $(basename "$src")" 5 ;;
esac
# Find the linux driver dir (contains a Makefile producing ajantv2.ko).
local drvdir
drvdir="$(dirname "$(find "$work" -type d -path '*driver/linux' -print -quit 2>/dev/null || true)/." 2>/dev/null)"
[ -d "$drvdir" ] || drvdir="$(dirname "$(find "$work" -name 'ajantv2.c' -print -quit 2>/dev/null || true)" 2>/dev/null)"
[ -n "$drvdir" ] && [ -d "$drvdir" ] || die "could not locate AJA driver/linux source inside the archive" 5
log "building module in $drvdir ..."
make -C "$drvdir" >/dev/null 2>&1 || die "AJA module build failed" 5
local ko
ko="$(find "$drvdir" -name 'ajantv2.ko' -print -quit 2>/dev/null || true)"
if [ -n "$ko" ]; then
install -D -m 0644 "$ko" "/lib/modules/$KVER/extra/ajantv2.ko" 2>/dev/null || true
depmod -a "$KVER" 2>/dev/null || true
fi
if ! modprobe ajantv2 2>/dev/null; then
# Fall back to the SDK's own load script if shipped.
local loader; loader="$(find "$work" -name 'load_ajantv2' -print -quit 2>/dev/null || true)"
if [ -n "$loader" ]; then bash "$loader" 2>/dev/null || true; fi
fi
rm -rf "$work" 2>/dev/null || true
if lsmod 2>/dev/null | grep -q 'ajantv2'; then
log "ajantv2 installed and loaded"
else
warn "ajantv2 built but not loaded — a reboot may be required (old in-tree module wedged?)"
REBOOT_REQUIRED=1
fi
}
# ===========================================================================
# deltacast — VideoMaster installer + module load
# ===========================================================================
install_deltacast() {
if lsmod 2>/dev/null | grep -q 'videomaster' || ls /dev/deltacast* >/dev/null 2>&1; then
log "Deltacast module loaded / device present — already installed"
install_deltacast_udev_rule
return 0
fi
local pkg
pkg="$(newest_match "$VENDOR_DIR/VideoMaster*.run")"
[ -n "$pkg" ] || pkg="$(newest_match "$VENDOR_DIR/VideoMaster*.tar.gz")"
[ -n "$pkg" ] && [ -f "$pkg" ] || die \
"no VideoMaster*.run / VideoMaster*.tar.gz in $VENDOR_DIR — obtain the Deltacast VideoMaster Linux installer and drop it there (see sdk/deltacast/README.md)." 3
log "using installer: $(basename "$pkg")"
ensure_headers
apt-get install -y build-essential dkms >/dev/null 2>&1 || true
local work; work="$(mktemp -d)"
case "$pkg" in
*.run)
log "extracting self-extractor ..."
chmod +x "$pkg" 2>/dev/null || true
"$pkg" --noexec --target "$work" >/dev/null 2>&1 \
|| cp "$pkg" "$work/installer.run"
;;
*.tar.gz)
tar -xzf "$pkg" -C "$work" || die "failed to untar $(basename "$pkg")" 5
;;
esac
local installer
installer="$(find "$work" -name 'install.sh' -print -quit 2>/dev/null || true)"
[ -n "$installer" ] || installer="$(find "$work" -name 'installer.run' -print -quit 2>/dev/null || true)"
[ -n "$installer" ] && [ -f "$installer" ] || die "Deltacast installer (install.sh) not found inside the package" 5
log "running vendor installer: $(basename "$installer") ..."
chmod +x "$installer" 2>/dev/null || true
( cd "$(dirname "$installer")" && bash "$installer" ) || die "Deltacast VideoMaster installer failed" 5
depmod -a "$KVER" 2>/dev/null || true
modprobe videomasterhd 2>/dev/null || modprobe videomaster 2>/dev/null || true
rm -rf "$work" 2>/dev/null || true
if lsmod 2>/dev/null | grep -q 'videomaster' || ls /dev/deltacast* >/dev/null 2>&1; then
log "Deltacast VideoMaster installed"
fi
# Install our own udev rule that creates 8 /dev/deltacast symlinks (ports 0-7)
# pointing at the single real device node. Kept separate from the SDK's own
# rule so a driver reinstall won't clobber it.
install_deltacast_udev_rule
# First-time VideoMaster installs lay down udev rules + firmware that need a reboot.
warn "Deltacast: a REBOOT is recommended after a first-time VideoMaster install (udev + firmware)"
REBOOT_REQUIRED=1
}
# Copy the repo's 99-wild-dragon-deltacast.rules into /etc/udev/rules.d/ and
# reload. Idempotent. Creates /dev/deltacast0..7 -> /dev/delta-x3700 so the
# node-agent advertises all 8 RX channels.
install_deltacast_udev_rule() {
local rule_src="$REPO_DIR/deploy/udev/99-wild-dragon-deltacast.rules"
local rule_dst="/etc/udev/rules.d/99-wild-dragon-deltacast.rules"
if [ ! -f "$rule_src" ]; then
warn "Deltacast: udev rule $rule_src not found in repo — skipping symlink rule install"
return 0
fi
if [ -f "$rule_dst" ] && cmp -s "$rule_src" "$rule_dst"; then
log "Deltacast: udev rule already up to date at $rule_dst"
else
log "installing Deltacast udev rule -> $rule_dst"
install -D -m 0644 "$rule_src" "$rule_dst" 2>/dev/null \
|| { warn "Deltacast: failed to install udev rule (continuing)"; return 0; }
udevadm control --reload-rules 2>/dev/null || true
udevadm trigger --action=add /dev/delta-x3700 2>/dev/null || true
fi
}
# ===========================================================================
# ndi — copy redistributable runtime libs to /usr/local/lib + ldconfig
# ===========================================================================
install_ndi() {
local target="/opt/ndi-lib"
local found=0
# shellcheck disable=SC2231
for f in "$VENDOR_DIR"/libndi*.so*; do
[ -e "$f" ] || continue
found=1
break
done
[ "$found" = 1 ] || die \
"no libndi*.so* in $VENDOR_DIR — drop the NDI runtime redistributable libs there (see sdk/ndi/README.md)." 3
log "copying NDI runtime libs to $target ..."
mkdir -p "$target"
cp -av "$VENDOR_DIR"/libndi*.so* "$target"/ 2>/dev/null || die "failed copying NDI libs" 5
# Recreate the libndi.so dev symlink if only versioned libs were shipped.
if [ ! -e "$target/libndi.so" ]; then
local versioned
versioned="$(newest_match "$target/libndi.so.*")"
if [ -n "$versioned" ]; then
ln -sf "$(basename "$versioned")" "$target/libndi.so" 2>/dev/null || true
fi
fi
echo "$target" > /etc/ld.so.conf.d/ndi.conf
ldconfig 2>/dev/null || true
if ldconfig -p 2>/dev/null | grep -q 'libndi'; then
log "NDI runtime registered with the dynamic linker"
else
die "NDI libs copied but ldconfig did not resolve libndi" 5
fi
log "NDI: no kernel module and no reboot required."
log "NDI: restart any process that already loaded an older libndi to pick up the new version."
}
# ---------------------------------------------------------------------------
case "$VENDOR" in
blackmagic) install_blackmagic ;;
aja) install_aja ;;
deltacast) install_deltacast ;;
ndi) install_ndi ;;
esac
if [ "$REBOOT_REQUIRED" = 1 ]; then
log "RESULT: $VENDOR install completed — REBOOT REQUIRED"
echo "[install-driver] REBOOT_REQUIRED=1"
else
log "RESULT: $VENDOR install completed — no reboot required"
echo "[install-driver] REBOOT_REQUIRED=0"
fi
exit 0

View file

@ -16,11 +16,12 @@
# Environment variables: # Environment variables:
# MAM_API_URL REQUIRED Primary MAM API base URL # MAM_API_URL REQUIRED Primary MAM API base URL
# NODE_TOKEN API bearer token (required if AUTH_ENABLED=true) # NODE_TOKEN API bearer token (required if AUTH_ENABLED=true)
# NODE_ROLE Role tag reported to the cluster (default: worker) # NODE_ROLE Role tag reported to the cluster (default: auto-detect)
# NODE_IP Override the LAN IP reported back (default: auto-detect) # NODE_IP Override the LAN IP reported back (default: auto-detect)
# AGENT_PORT Host port for the node agent (default: 7436) # AGENT_PORT Host port for the node agent (default: 7436)
# INSTALL_DIR Where to clone/find the repo (default: /opt/wild-dragon) # INSTALL_DIR Where to clone/find the repo (default: /opt/wild-dragon)
# PROFILES Extra compose profiles, space-sep e.g. "worker capture" # PROFILES Compose profiles, space-sep (default: auto-detect from hardware)
# Override only to force, e.g. "worker capture"
# BMD_MODEL DeckLink card model name (e.g. "DeckLink Duo 2") # BMD_MODEL DeckLink card model name (e.g. "DeckLink Duo 2")
# REPO_URL Override the Forgejo clone URL # REPO_URL Override the Forgejo clone URL
# ============================================================================= # =============================================================================
@ -32,8 +33,16 @@ REPO_URL="${REPO_URL:-https://forge.wilddragon.net/zgaetano/wild-dragon.git}"
INSTALL_DIR="${INSTALL_DIR:-/opt/wild-dragon}" INSTALL_DIR="${INSTALL_DIR:-/opt/wild-dragon}"
MAM_API_URL="${MAM_API_URL:-}" MAM_API_URL="${MAM_API_URL:-}"
NODE_TOKEN="${NODE_TOKEN:-}" NODE_TOKEN="${NODE_TOKEN:-}"
# Track whether the caller pinned NODE_ROLE explicitly (manual override) vs.
# us defaulting it — so auto-detection only fills in an *unset* role.
[[ -n "${NODE_ROLE:-}" ]] && NODE_ROLE_EXPLICIT=1 || NODE_ROLE_EXPLICIT=""
NODE_ROLE="${NODE_ROLE:-worker}" NODE_ROLE="${NODE_ROLE:-worker}"
NODE_IP="${NODE_IP:-}" NODE_IP="${NODE_IP:-}"
# NODE_NAME pins this node's cluster identity (the heartbeat key). Default to the
# OS hostname, but ALWAYS write it explicitly so cloned VMs that share an
# /etc/hostname (e.g. two boxes both named "zampp1") don't collide on the same
# cluster_nodes row — which silently hides the capture node's DeckLink devices.
NODE_NAME="${NODE_NAME:-$(hostname)}"
AGENT_PORT="${AGENT_PORT:-7436}" AGENT_PORT="${AGENT_PORT:-7436}"
PROFILES="${PROFILES:-}" PROFILES="${PROFILES:-}"
BMD_MODEL="${BMD_MODEL:-}" BMD_MODEL="${BMD_MODEL:-}"
@ -65,6 +74,37 @@ detect_lan_ip() {
echo "$ip" echo "$ip"
} }
# ── Auto-detect hardware ─────────────────────────────────────────────────────
# Mirror detect_lan_ip's style: best-effort, guard every probe with `|| true`
# so a missing nvidia-smi/lspci never aborts under `set -euo pipefail`. The
# node self-describes its hardware here so the operator never has to pick a
# role — the right compose profiles are enabled automatically.
# GPU present? nvidia-smi is the strong signal; fall back to an lspci scan for
# NVIDIA or AMD VGA controllers (covers nodes where the driver isn't installed
# yet but the card is physically present).
detect_gpu() {
if command -v nvidia-smi &>/dev/null && nvidia-smi -L &>/dev/null; then
return 0
fi
if command -v lspci &>/dev/null; then
if lspci 2>/dev/null | grep -iE 'nvidia|vga.*amd' &>/dev/null; then
return 0
fi
fi
return 1
}
# SDI capture card present? Blackmagic DeckLink or Deltacast, via lspci.
detect_sdi() {
if command -v lspci &>/dev/null; then
if lspci 2>/dev/null | grep -iE 'blackmagic|deltacast' &>/dev/null; then
return 0
fi
fi
return 1
}
# ── Preflight ──────────────────────────────────────────────────────────────── # ── Preflight ────────────────────────────────────────────────────────────────
echo -e "\n${BLD}${CYN}Wild Dragon MAM — Cluster Node Onboarding${NC}\n" echo -e "\n${BLD}${CYN}Wild Dragon MAM — Cluster Node Onboarding${NC}\n"
@ -79,6 +119,36 @@ if [[ -z "$NODE_IP" ]]; then
fi fi
fi fi
# ── Auto-assign compose profiles from detected hardware ──────────────────────
# Operator never picks a role: the worker profile always runs, and we add the
# gpu / capture profiles only when the matching hardware is present. Explicit
# PROFILES / NODE_ROLE env vars are honoured as a manual override escape hatch.
HAS_GPU=false; HAS_SDI=false
detect_gpu && HAS_GPU=true || true
detect_sdi && HAS_SDI=true || true
DETECTED_DESC="CPU"
[[ "$HAS_GPU" == true ]] && DETECTED_DESC="$DETECTED_DESC, GPU"
[[ "$HAS_SDI" == true ]] && DETECTED_DESC="$DETECTED_DESC, SDI capture card"
if [[ -z "$PROFILES" ]]; then
AUTO_PROFILES="worker"
[[ "$HAS_GPU" == true ]] && AUTO_PROFILES="$AUTO_PROFILES gpu"
[[ "$HAS_SDI" == true ]] && AUTO_PROFILES="$AUTO_PROFILES capture"
PROFILES="$AUTO_PROFILES"
info "Detected: $DETECTED_DESC → profiles: $PROFILES"
else
info "Detected: $DETECTED_DESC (profiles overridden by env: $PROFILES)"
fi
# Derive a human-friendly role tag from detected hardware when not pinned.
# Capture cards win over GPU (an SDI ingest node is the more specific role).
if [[ -z "$NODE_ROLE_EXPLICIT" ]]; then
if [[ "$HAS_SDI" == true ]]; then NODE_ROLE="capture"
elif [[ "$HAS_GPU" == true ]]; then NODE_ROLE="gpu"
else NODE_ROLE="worker"; fi
fi
info "Primary API : $MAM_API_URL" info "Primary API : $MAM_API_URL"
info "Role : $NODE_ROLE" info "Role : $NODE_ROLE"
info "Agent port : $AGENT_PORT" info "Agent port : $AGENT_PORT"
@ -135,6 +205,7 @@ info "Writing $ENV_FILE"
echo "MAM_API_URL=$MAM_API_URL" echo "MAM_API_URL=$MAM_API_URL"
echo "NODE_TOKEN=$NODE_TOKEN" echo "NODE_TOKEN=$NODE_TOKEN"
echo "NODE_ROLE=$NODE_ROLE" echo "NODE_ROLE=$NODE_ROLE"
echo "NODE_NAME=$NODE_NAME"
echo "NODE_IP=$NODE_IP" echo "NODE_IP=$NODE_IP"
echo "AGENT_PORT=$AGENT_PORT" echo "AGENT_PORT=$AGENT_PORT"
echo "HEARTBEAT_MS=30000" echo "HEARTBEAT_MS=30000"

View file

@ -0,0 +1 @@
KERNEL=="delta-x3700", MODE="0666", RUN+="/bin/sh -c 'for i in 0 1 2 3 4 5 6 7; do ln -sf /dev/delta-x3700 /dev/deltacast$i; done'"

View file

@ -47,6 +47,10 @@ services:
MAM_API_URL: ${MAM_API_URL} MAM_API_URL: ${MAM_API_URL}
NODE_TOKEN: ${NODE_TOKEN:-} NODE_TOKEN: ${NODE_TOKEN:-}
NODE_ROLE: ${NODE_ROLE:-worker} NODE_ROLE: ${NODE_ROLE:-worker}
# NODE_NAME pins the cluster identity (heartbeat key). Set it per-node so
# cloned VMs that share /etc/hostname don't collide on the same
# cluster_nodes row. Falls back to the OS hostname when unset.
NODE_NAME: ${NODE_NAME:-}
NODE_IP: ${NODE_IP:-} NODE_IP: ${NODE_IP:-}
AGENT_PORT: ${AGENT_PORT:-7436} AGENT_PORT: ${AGENT_PORT:-7436}
HEARTBEAT_MS: ${HEARTBEAT_MS:-30000} HEARTBEAT_MS: ${HEARTBEAT_MS:-30000}
@ -55,10 +59,25 @@ services:
BMD_MODEL: ${BMD_MODEL:-} BMD_MODEL: ${BMD_MODEL:-}
BMD_DEVICE_PREFIX: ${BMD_DEVICE_PREFIX:-dv} BMD_DEVICE_PREFIX: ${BMD_DEVICE_PREFIX:-dv}
LIVE_DIR: ${LIVE_DIR:-/mnt/NVME/MAM/wild-dragon-live} LIVE_DIR: ${LIVE_DIR:-/mnt/NVME/MAM/wild-dragon-live}
# REPO_DIR: host path to the checked-out repo. The agent passes this to the
# one-shot driver-install container so install-driver.sh can read
# sdk/<vendor>/ and run deploy/install-driver.sh. Must match the host path
# bind-mounted below (onboard-node.sh clones to /opt/wild-dragon).
REPO_DIR: ${REPO_DIR:-/opt/wild-dragon}
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
- /dev:/dev:ro - /dev:/dev:ro
- /mnt/NVME/MAM/wild-dragon-live:/mnt/NVME/MAM/wild-dragon-live:ro - /mnt/NVME/MAM/wild-dragon-live:/mnt/NVME/MAM/wild-dragon-live:ro
# Capture-driver deployment ("Capture Drivers / SDKs" in the Cluster admin
# screen): the agent itself does NOT run dkms/modprobe — it spawns a
# separate privileged ubuntu container that bind-mounts these host paths.
# The agent only needs to *see* the repo path so it can pass it through as
# a bind to that container; no extra privileges are granted to the agent.
# /opt/wild-dragon → repo (sdk/<vendor>/ + deploy/install-driver.sh)
# The install container additionally mounts /lib/modules,/usr/src,/boot,
# /dev and /opt from the host (handled in the agent, not here) so DKMS /
# modprobe / ldconfig affect the host kernel.
- ${REPO_DIR:-/opt/wild-dragon}:${REPO_DIR:-/opt/wild-dragon}:ro
devices: devices:
- /dev/blackmagic:/dev/blackmagic - /dev/blackmagic:/dev/blackmagic

View file

@ -61,6 +61,11 @@ services:
DOCKER_NETWORK: wild-dragon_wild-dragon DOCKER_NETWORK: wild-dragon_wild-dragon
NODE_IP: ${NODE_IP} NODE_IP: ${NODE_IP}
NODE_HOSTNAME: ${NODE_HOSTNAME:-} NODE_HOSTNAME: ${NODE_HOSTNAME:-}
# Bearer mam-api forwards to a node-agent when installing capture drivers
# ("Capture Drivers / SDKs" panel). Set to the same value as the agents'
# NODE_TOKEN. If empty, agents with an empty NODE_TOKEN accept the call
# (dev); agents with a token will reject it (401).
NODE_AGENT_TOKEN: ${NODE_AGENT_TOKEN:-}
CAPTURE_TOKEN: ${CAPTURE_TOKEN} CAPTURE_TOKEN: ${CAPTURE_TOKEN}
PLAYOUT_IMAGE: ${PLAYOUT_IMAGE:-wild-dragon-playout:latest} PLAYOUT_IMAGE: ${PLAYOUT_IMAGE:-wild-dragon-playout:latest}
PLAYOUT_AMCP_BASE_PORT: ${PLAYOUT_AMCP_BASE_PORT:-5250} PLAYOUT_AMCP_BASE_PORT: ${PLAYOUT_AMCP_BASE_PORT:-5250}
@ -107,6 +112,11 @@ services:
dockerfile: Dockerfile.gpu dockerfile: Dockerfile.gpu
image: wild-dragon-worker-gpu:latest image: wild-dragon-worker-gpu:latest
runtime: nvidia runtime: nvidia
# Privileged so the promotion scanner can mount the growing-files CIFS share
# at /growing (same Approach A as the capture sidecar). Without the share
# mounted the scanner watches an empty local dir and never promotes growing
# captures to S3.
privileged: true
depends_on: depends_on:
- queue - queue
- db - db
@ -131,7 +141,9 @@ services:
WORKER_LABEL: "zampp1 / Tesla P4" WORKER_LABEL: "zampp1 / Tesla P4"
NVIDIA_DRIVER_CAPABILITIES: video,compute,utility NVIDIA_DRIVER_CAPABILITIES: video,compute,utility
volumes: volumes:
- /mnt/NVME/MAM/wild-dragon-growing:/growing # NOTE: /growing is NOT a host bind anymore — the promotion scanner mounts
# the CIFS landing-zone share there itself (a bind would shadow it). The
# mount needs rshared propagation so the in-container CIFS mount is visible.
- /mnt/NVME/MAM/wild-dragon-media:/media - /mnt/NVME/MAM/wild-dragon-media:/media
networks: networks:
- wild-dragon - wild-dragon

View file

@ -0,0 +1,834 @@
# Deltacast SDI Capture — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Wire up Deltacast VideoMaster SDI cards in the capture service using a C bridge binary that streams raw video to FFmpeg via pipe, with embedded audio via a named FIFO.
**Architecture:** A `deltacast-capture` C binary opens the VideoMaster board, waits for signal lock, emits a JSON format line to stderr, then streams raw UYVY video frames to stdout and 2-channel PCM audio to a named FIFO. `capture-manager.js` reads the JSON, spawns FFmpeg with `-f rawvideo -i pipe:0` for video and `-f s16le -i <fifo>` for audio, and pipes bridge stdout into FFmpeg stdin. Two concurrent SDK streams share the same board handle — `VHD_SDI_STPROC_DISJOINED_VIDEO` for video and `VHD_SDI_STPROC_DISJOINED_ANC` for audio.
**Tech Stack:** Deltacast VideoMaster C SDK 6.34.1 (`libvideomasterhd.so`, `libvideomasterhd_audio.so`), C17, CMake, Node.js ES modules, Docker multi-stage build.
---
## File Map
| Action | Path | Responsibility |
|---|---|---|
| Create | `services/capture/deltacast-bridge/CMakeLists.txt` | Build config for the bridge binary |
| Create | `services/capture/deltacast-bridge/main.c` | Bridge: board open, signal detect, video stream, audio thread |
| Modify | `services/capture/Dockerfile` | SDK extraction stage, bridge build stage, runtime .so install |
| Modify | `services/capture/src/capture-manager.js` | `readFirstStderrLine` helper, deltacast `_buildInputArgs`, bridge lifecycle in `start()`/`stop()` |
| Modify | `services/capture/src/routes/capture.js` | Accept `deltacast` as a valid `source_type` |
---
## Task 1: Bridge CMakeLists.txt
**Files:**
- Create: `services/capture/deltacast-bridge/CMakeLists.txt`
- [ ] **Step 1: Create the CMakeLists.txt**
```cmake
cmake_minimum_required(VERSION 3.16)
project(deltacast-bridge C)
set(CMAKE_C_STANDARD 17)
set(SDK_ROOT "/sdk" CACHE PATH "Path to extracted VideoMaster SDK")
add_executable(deltacast-capture main.c)
target_include_directories(deltacast-capture PRIVATE
${SDK_ROOT}/include/videomaster
)
target_link_directories(deltacast-capture PRIVATE
${SDK_ROOT}/lib
)
target_link_libraries(deltacast-capture PRIVATE
videomasterhd
videomasterhd_audio
pthread
)
# Embed the SDK RPATH so the binary finds the .so at runtime
set_target_properties(deltacast-capture PROPERTIES
INSTALL_RPATH "/usr/local/lib/deltacast"
BUILD_WITH_INSTALL_RPATH TRUE
)
```
- [ ] **Step 2: Commit**
```bash
git add services/capture/deltacast-bridge/CMakeLists.txt
git commit -m "build(capture): add CMakeLists for deltacast-capture bridge binary"
```
---
## Task 2: Bridge main.c
**Files:**
- Create: `services/capture/deltacast-bridge/main.c`
The binary: parses CLI args, opens the board, waits for signal lock, emits one JSON line to stderr, then spawns an audio thread writing to a FIFO and runs a video capture loop writing raw UYVY frames to stdout.
- [ ] **Step 1: Create the bridge source file**
```c
/* services/capture/deltacast-bridge/main.c
*
* Deltacast VideoMaster SDI capture bridge.
* Writes raw UYVY video to stdout and stereo PCM to a named FIFO.
* Emits one JSON line to stderr on signal lock before streaming starts.
*
* Usage:
* deltacast-capture --device <N> --port <N> --audio-pipe <path>
* [--signal-timeout <sec>]
*/
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <signal.h>
#include <stdatomic.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include "VideoMasterHD_Core.h"
#include "VideoMasterHD_Sdi.h"
#include "VideoMasterHD_Sdi_Audio.h"
/* ── Globals ─────────────────────────────────────────────────────────── */
static atomic_int g_stop = 0;
static void on_signal(int s) { (void)s; atomic_store(&g_stop, 1); }
/* ── Stream type by port index ───────────────────────────────────────── */
static ULONG rx_streamtype(unsigned port) {
switch (port) {
case 0: return VHD_ST_RX0;
case 1: return VHD_ST_RX1;
case 2: return VHD_ST_RX2;
case 3: return VHD_ST_RX3;
default: return VHD_ST_RX0;
}
}
/* ── Loopback board property by port index ───────────────────────────── */
static ULONG loopback_prop(unsigned port) {
switch (port) {
case 0: return VHD_CORE_BP_PASSIVE_LOOPBACK_0;
case 1: return VHD_CORE_BP_PASSIVE_LOOPBACK_1;
case 2: return VHD_CORE_BP_PASSIVE_LOOPBACK_2;
case 3: return VHD_CORE_BP_PASSIVE_LOOPBACK_3;
default: return VHD_CORE_BP_PASSIVE_LOOPBACK_0;
}
}
/* ── Video standard → width/height/fps/interlaced ───────────────────── */
typedef struct { int width, height, fps_num, fps_den; int interlaced; } VideoInfo;
static VideoInfo video_info(VHD_VIDEOSTANDARD std, VHD_CLOCKDIVISOR div) {
int ntsc = (div == VHD_CLOCKDIV_1001);
switch (std) {
case VHD_VIDEOSTD_S274M_1080p_25Hz: return (VideoInfo){1920,1080,25,1,0};
case VHD_VIDEOSTD_S274M_1080p_30Hz: return (VideoInfo){1920,1080,ntsc?30000:30,ntsc?1001:1,0};
case VHD_VIDEOSTD_S274M_1080p_24Hz: return (VideoInfo){1920,1080,ntsc?24000:24,ntsc?1001:1,0};
case VHD_VIDEOSTD_S274M_1080p_50Hz: return (VideoInfo){1920,1080,50,1,0};
case VHD_VIDEOSTD_S274M_1080p_60Hz: return (VideoInfo){1920,1080,ntsc?60000:60,ntsc?1001:1,0};
case VHD_VIDEOSTD_S274M_1080psf_24Hz: return (VideoInfo){1920,1080,ntsc?24000:24,ntsc?1001:1,0};
case VHD_VIDEOSTD_S274M_1080psf_25Hz: return (VideoInfo){1920,1080,25,1,0};
case VHD_VIDEOSTD_S274M_1080psf_30Hz: return (VideoInfo){1920,1080,ntsc?30000:30,ntsc?1001:1,0};
case VHD_VIDEOSTD_S274M_1080i_50Hz: return (VideoInfo){1920,1080,25,1,1};
case VHD_VIDEOSTD_S274M_1080i_60Hz: return (VideoInfo){1920,1080,ntsc?30000:30,ntsc?1001:1,1};
case VHD_VIDEOSTD_S296M_720p_50Hz: return (VideoInfo){1280,720,50,1,0};
case VHD_VIDEOSTD_S296M_720p_60Hz: return (VideoInfo){1280,720,ntsc?60000:60,ntsc?1001:1,0};
case VHD_VIDEOSTD_S296M_720p_25Hz: return (VideoInfo){1280,720,25,1,0};
case VHD_VIDEOSTD_S296M_720p_30Hz: return (VideoInfo){1280,720,ntsc?30000:30,ntsc?1001:1,0};
case VHD_VIDEOSTD_S296M_720p_24Hz: return (VideoInfo){1280,720,ntsc?24000:24,ntsc?1001:1,0};
case VHD_VIDEOSTD_3840x2160p_24Hz: return (VideoInfo){3840,2160,ntsc?24000:24,ntsc?1001:1,0};
case VHD_VIDEOSTD_3840x2160p_25Hz: return (VideoInfo){3840,2160,25,1,0};
case VHD_VIDEOSTD_3840x2160p_30Hz: return (VideoInfo){3840,2160,ntsc?30000:30,ntsc?1001:1,0};
case VHD_VIDEOSTD_3840x2160p_50Hz: return (VideoInfo){3840,2160,50,1,0};
case VHD_VIDEOSTD_3840x2160p_60Hz: return (VideoInfo){3840,2160,ntsc?60000:60,ntsc?1001:1,0};
case VHD_VIDEOSTD_S259M_NTSC_480: return (VideoInfo){720,480,ntsc?30000:30,ntsc?1001:1,1};
default: return (VideoInfo){1920,1080,25,1,0};
}
}
/* ── Audio thread ────────────────────────────────────────────────────── */
typedef struct {
HANDLE board;
unsigned port;
ULONG video_std;
ULONG clock_div;
const char *fifo_path;
} AudioArgs;
static void *audio_thread(void *arg) {
AudioArgs *a = (AudioArgs *)arg;
HANDLE stream = NULL;
ULONG r = VHD_OpenStreamHandle(a->board, rx_streamtype(a->port),
VHD_SDI_STPROC_DISJOINED_ANC,
NULL, &stream, NULL);
if (r != VHDERR_NOERROR) {
fprintf(stderr, "[audio] VHD_OpenStreamHandle failed: %lu\n", r);
return NULL;
}
VHD_SetStreamProperty(stream, VHD_SDI_SP_VIDEO_STANDARD, a->video_std);
VHD_SetStreamProperty(stream, VHD_SDI_SP_CLOCK_SYSTEM, a->clock_div);
VHD_SetStreamProperty(stream, VHD_CORE_SP_TRANSFER_SCHEME, VHD_TRANSFER_SLAVED);
/* Stereo pair, 16-bit, 48kHz on group 0 channel 0 */
ULONG max_samples = VHD_GetNbSamples((VHD_VIDEOSTANDARD)a->video_std,
(VHD_CLOCKDIVISOR)a->clock_div,
VHD_ASR_48000, 0);
ULONG block_size = VHD_GetBlockSize(VHD_AF_16, VHD_AM_STEREO);
ULONG buf_sz = (max_samples + 4) * block_size; /* +4 for 29.97 variation */
unsigned char *buf = calloc(1, buf_sz);
if (!buf) { VHD_CloseStreamHandle(stream); return NULL; }
VHD_AUDIOINFO ai;
memset(&ai, 0, sizeof(ai));
ai.pAudioGroups[0].pAudioChannels[0].Mode = VHD_AM_STEREO;
ai.pAudioGroups[0].pAudioChannels[0].BufferFormat = VHD_AF_16;
ai.pAudioGroups[0].pAudioChannels[0].pData = buf;
if (VHD_StartStream(stream) != VHDERR_NOERROR) {
free(buf); VHD_CloseStreamHandle(stream); return NULL;
}
/* Open FIFO for writing — blocks until FFmpeg opens the read end */
int fd = open(a->fifo_path, O_WRONLY);
if (fd < 0) {
fprintf(stderr, "[audio] open FIFO failed: %s\n", strerror(errno));
VHD_StopStream(stream); VHD_CloseStreamHandle(stream); free(buf);
return NULL;
}
HANDLE slot = NULL;
while (!atomic_load(&g_stop)) {
r = VHD_LockSlotHandle(stream, &slot);
if (r == VHDERR_NOERROR) {
ai.pAudioGroups[0].pAudioChannels[0].DataSize = buf_sz;
if (VHD_SlotExtractAudio(slot, &ai) == VHDERR_NOERROR) {
ULONG sz = ai.pAudioGroups[0].pAudioChannels[0].DataSize;
if (sz > 0) write(fd, buf, sz);
}
VHD_UnlockSlotHandle(slot);
} else if (r != VHDERR_TIMEOUT) {
break;
}
}
close(fd);
VHD_StopStream(stream);
VHD_CloseStreamHandle(stream);
free(buf);
return NULL;
}
/* ── Main ────────────────────────────────────────────────────────────── */
int main(int argc, char *argv[]) {
unsigned device_id = 0;
unsigned port_id = 0;
int sig_timeout = 30;
const char *audio_pipe = NULL;
for (int i = 1; i < argc; i++) {
if (!strcmp(argv[i], "--device") && i+1 < argc) device_id = (unsigned)atoi(argv[++i]);
else if (!strcmp(argv[i], "--port") && i+1 < argc) port_id = (unsigned)atoi(argv[++i]);
else if (!strcmp(argv[i], "--audio-pipe") && i+1 < argc) audio_pipe = argv[++i];
else if (!strcmp(argv[i], "--signal-timeout") && i+1 < argc) sig_timeout = atoi(argv[++i]);
}
signal(SIGINT, on_signal);
signal(SIGTERM, on_signal);
/* ── Init API ─────────────────────────────────────────────────── */
ULONG dll_ver, nb_boards;
if (VHD_GetApiInfo(&dll_ver, &nb_boards) != VHDERR_NOERROR) {
fprintf(stderr, "{\"error\":\"VHD_GetApiInfo failed\"}\n");
return 1;
}
if (device_id >= nb_boards) {
fprintf(stderr, "{\"error\":\"board %u not found (%lu detected)\"}\n", device_id, nb_boards);
return 1;
}
/* ── Open board ───────────────────────────────────────────────── */
HANDLE board = NULL;
if (VHD_OpenBoardHandle(device_id, &board, NULL, 0) != VHDERR_NOERROR) {
fprintf(stderr, "{\"error\":\"VHD_OpenBoardHandle failed for board %u\"}\n", device_id);
return 1;
}
/* Disable passive (relay) loopback so RX is live */
VHD_SetBoardProperty(board, loopback_prop(port_id), FALSE);
/* ── Wait for signal lock ──────────────────────────────────────── */
ULONG video_std = (ULONG)NB_VHD_VIDEOSTANDARDS;
struct timespec deadline;
clock_gettime(CLOCK_MONOTONIC, &deadline);
deadline.tv_sec += sig_timeout;
while (!atomic_load(&g_stop)) {
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
if (now.tv_sec > deadline.tv_sec ||
(now.tv_sec == deadline.tv_sec && now.tv_nsec >= deadline.tv_nsec)) break;
VHD_GetChannelProperty(board, VHD_RX_CHANNEL, port_id,
VHD_SDI_CP_VIDEO_STANDARD, &video_std);
if (video_std != (ULONG)NB_VHD_VIDEOSTANDARDS) break;
struct timespec ts = {0, 200000000L}; /* 200ms */
nanosleep(&ts, NULL);
}
if (atomic_load(&g_stop) || video_std == (ULONG)NB_VHD_VIDEOSTANDARDS) {
fprintf(stderr,
"{\"error\":\"no signal on board %u port %u within %ds\"}\n",
device_id, port_id, sig_timeout);
VHD_CloseBoardHandle(board);
return 1;
}
ULONG clock_div = VHD_CLOCKDIV_1;
VHD_GetChannelProperty(board, VHD_RX_CHANNEL, port_id,
VHD_SDI_CP_CLOCK_DIVISOR, &clock_div);
VideoInfo vi = video_info((VHD_VIDEOSTANDARD)video_std,
(VHD_CLOCKDIVISOR)clock_div);
/* ── Emit format JSON to stderr (one line, flushed) ─────────────── */
fprintf(stderr,
"{\"width\":%d,\"height\":%d,\"fps_num\":%d,\"fps_den\":%d,"
"\"interlaced\":%s,\"pix_fmt\":\"uyvy422\","
"\"audio_channels\":2,\"audio_rate\":48000,"
"\"device\":%u,\"port\":%u}\n",
vi.width, vi.height, vi.fps_num, vi.fps_den,
vi.interlaced ? "true" : "false",
device_id, port_id);
fflush(stderr);
/* ── Open video stream ───────────────────────────────────────────── */
HANDLE video_stream = NULL;
if (VHD_OpenStreamHandle(board, rx_streamtype(port_id),
VHD_SDI_STPROC_DISJOINED_VIDEO,
NULL, &video_stream, NULL) != VHDERR_NOERROR) {
fprintf(stderr, "{\"error\":\"VHD_OpenStreamHandle (video) failed\"}\n");
VHD_CloseBoardHandle(board);
return 1;
}
VHD_SetStreamProperty(video_stream, VHD_SDI_SP_VIDEO_STANDARD, video_std);
VHD_SetStreamProperty(video_stream, VHD_SDI_SP_CLOCK_SYSTEM, clock_div);
VHD_SetStreamProperty(video_stream, VHD_CORE_SP_TRANSFER_SCHEME, VHD_TRANSFER_SLAVED);
VHD_SetStreamProperty(video_stream, VHD_CORE_SP_SLOTS_QUEUE_DEPTH, 8);
/* ── Launch audio thread (FIFO open blocks until FFmpeg connects) ── */
pthread_t audio_tid = 0;
AudioArgs audio_args = { board, port_id, video_std, clock_div, audio_pipe };
if (audio_pipe) {
pthread_create(&audio_tid, NULL, audio_thread, &audio_args);
}
/* ── Start video stream ──────────────────────────────────────────── */
if (VHD_StartStream(video_stream) != VHDERR_NOERROR) {
atomic_store(&g_stop, 1);
if (audio_tid) pthread_join(audio_tid, NULL);
VHD_CloseStreamHandle(video_stream);
VHD_CloseBoardHandle(board);
return 1;
}
/* ── Video capture loop ──────────────────────────────────────────── */
HANDLE slot = NULL;
while (!atomic_load(&g_stop)) {
ULONG r = VHD_LockSlotHandle(video_stream, &slot);
if (r == VHDERR_NOERROR) {
BYTE *buf = NULL;
ULONG sz = 0;
if (VHD_GetSlotBuffer(slot, VHD_SDI_BT_VIDEO, &buf, &sz) == VHDERR_NOERROR) {
ULONG written = 0;
while (written < sz) {
ssize_t n = write(STDOUT_FILENO, buf + written, sz - written);
if (n <= 0) { atomic_store(&g_stop, 1); break; }
written += (ULONG)n;
}
}
VHD_UnlockSlotHandle(slot);
} else if (r != VHDERR_TIMEOUT) {
break;
}
}
/* ── Cleanup ─────────────────────────────────────────────────────── */
VHD_StopStream(video_stream);
VHD_CloseStreamHandle(video_stream);
if (audio_tid) pthread_join(audio_tid, NULL);
VHD_CloseBoardHandle(board);
return 0;
}
```
- [ ] **Step 2: Commit**
```bash
git add services/capture/deltacast-bridge/main.c
git commit -m "feat(capture): add deltacast-capture bridge binary source"
```
---
## Task 3: Dockerfile — SDK extraction + bridge build + runtime
**Files:**
- Modify: `services/capture/Dockerfile`
The existing Dockerfile has three logical sections: FFmpeg build, runtime. We add two new stages before FFmpeg and patch the runtime stage.
- [ ] **Step 1: Read the current Dockerfile**
Read `services/capture/Dockerfile` and verify it starts with `FROM debian:bookworm AS ffmpeg-builder`.
- [ ] **Step 2: Prepend two new stages and patch runtime**
The full new Dockerfile:
```dockerfile
# ── Stage 0: Extract Deltacast VideoMaster SDK ───────────────────────────
FROM debian:bookworm AS sdk-extractor
COPY videomaster-linux.x64-6.34.1-dev.tar.gz /tmp/
RUN mkdir -p /sdk && tar -xzf /tmp/videomaster-linux.x64-6.34.1-dev.tar.gz -C /sdk
# ── Stage 1: Build deltacast-capture bridge binary ───────────────────────
FROM debian:bookworm AS bridge-builder
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential cmake ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY --from=sdk-extractor /sdk /sdk
COPY deltacast-bridge/ /bridge/
RUN cmake -S /bridge -B /bridge/build \
-DCMAKE_BUILD_TYPE=Release \
-DSDK_ROOT=/sdk \
&& cmake --build /bridge/build -j$(nproc)
# ── Stage 2: Build FFmpeg with DeckLink + NVENC (HEVC/H264) support ──────
# (unchanged — keep original content here)
FROM debian:bookworm AS ffmpeg-builder
# ... (rest of the existing ffmpeg-builder stage unchanged) ...
# ── Stage 3: Runtime image ───────────────────────────────────────────────
FROM node:20-bookworm
# Runtime deps for compiled ffmpeg libs (unchanged)
RUN apt-get update && apt-get install -y --no-install-recommends \
libx264-164 libx265-199 libvpx7 libopus0 libmp3lame0 \
libsrt1.5-openssl libzmq5 libstdc++6 libc++1 libc++abi1 \
&& rm -rf /var/lib/apt/lists/*
# Copy compiled ffmpeg/ffprobe (unchanged)
COPY --from=ffmpeg-builder /usr/local/bin/ffmpeg /usr/local/bin/ffmpeg
COPY --from=ffmpeg-builder /usr/local/bin/ffprobe /usr/local/bin/ffprobe
COPY --from=ffmpeg-builder /usr/local/lib/ /usr/local/lib/
# DeckLink runtime .so (unchanged)
COPY lib/libDeckLinkAPI.so /usr/lib/libDeckLinkAPI.so
COPY lib/libDeckLinkPreviewAPI.so /usr/lib/libDeckLinkPreviewAPI.so
# Deltacast bridge binary + SDK runtime libs
COPY --from=bridge-builder /bridge/build/deltacast-capture /usr/local/bin/deltacast-capture
COPY --from=sdk-extractor /sdk/lib/libvideomasterhd.so.6.34.1 /usr/local/lib/deltacast/
COPY --from=sdk-extractor /sdk/lib/libvideomasterhd_audio.so.6.34.1 /usr/local/lib/deltacast/
RUN ln -sf libvideomasterhd.so.6.34.1 /usr/local/lib/deltacast/libvideomasterhd.so.6 \
&& ln -sf libvideomasterhd.so.6.34.1 /usr/local/lib/deltacast/libvideomasterhd.so \
&& ln -sf libvideomasterhd_audio.so.6.34.1 /usr/local/lib/deltacast/libvideomasterhd_audio.so.6 \
&& ln -sf libvideomasterhd_audio.so.6.34.1 /usr/local/lib/deltacast/libvideomasterhd_audio.so \
&& ldconfig /usr/local/lib/deltacast \
&& ldconfig
RUN mkdir -p /live /growing
WORKDIR /app
COPY package*.json ./
RUN npm install --omit=dev
COPY . .
EXPOSE 3001
CMD ["node", "src/index.js"]
```
**Implementation note:** Edit the existing Dockerfile. Prepend the two new FROM stages (sdk-extractor, bridge-builder) before the existing `FROM debian:bookworm AS ffmpeg-builder` line. Then in the final runtime stage, add the Deltacast `COPY` and `RUN` lines after the DeckLink `.so` lines (before the `RUN mkdir -p /live /growing` line).
- [ ] **Step 3: Commit**
```bash
git add services/capture/Dockerfile
git commit -m "build(capture): add Deltacast SDK extraction and bridge build stages to Dockerfile"
```
---
## Task 4: capture-manager.js — `readFirstStderrLine` helper
**Files:**
- Modify: `services/capture/src/capture-manager.js` (add helper near top, after imports)
- [ ] **Step 1: Add the helper function after the existing imports (after line 6 `import { v4 as uuidv4 } from 'uuid';`)**
```js
/**
* Reads the first line from a spawned process's stderr stream.
* Resolves with the parsed JSON object when the first '\n' arrives.
* Rejects if the process exits with a non-zero code before emitting a line,
* or if timeoutMs elapses.
*/
function readFirstStderrLine(proc, timeoutMs = 35_000) {
return new Promise((resolve, reject) => {
let buf = '';
let settled = false;
const settle = (fn) => { if (settled) return; settled = true; fn(); };
const timer = setTimeout(() => {
settle(() => reject(new Error(`deltacast-capture: timed out waiting for format JSON after ${timeoutMs}ms`)));
}, timeoutMs);
proc.stderr.setEncoding('utf8');
proc.stderr.on('data', (chunk) => {
buf += chunk;
const nl = buf.indexOf('\n');
if (nl === -1) return;
const line = buf.slice(0, nl).trim();
clearTimeout(timer);
try {
const parsed = JSON.parse(line);
if (parsed.error) {
settle(() => reject(new Error(`deltacast-capture: ${parsed.error}`)));
} else {
settle(() => resolve(parsed));
}
} catch (e) {
settle(() => reject(new Error(`deltacast-capture: invalid JSON on stderr: ${line}`)));
}
});
proc.on('exit', (code) => {
clearTimeout(timer);
settle(() => reject(new Error(`deltacast-capture: exited with code ${code} before emitting format JSON`)));
});
});
}
```
- [ ] **Step 2: Commit**
```bash
git add services/capture/src/capture-manager.js
git commit -m "feat(capture): add readFirstStderrLine helper for deltacast bridge handshake"
```
---
## Task 5: capture-manager.js — Deltacast `_buildInputArgs`
**Files:**
- Modify: `services/capture/src/capture-manager.js` — replace the `deltacast` branch of `_buildInputArgs` (currently lines 160191)
- [ ] **Step 1: Replace the existing deltacast branch**
Find the block starting with `// Deltacast SDI via VideoMaster SDK FFmpeg plugin.` and ending at the closing `}` of the `if (sourceType === 'deltacast')` block. Replace the entire `if (sourceType === 'deltacast') { ... }` block with:
```js
if (sourceType === 'deltacast') {
const idx = (typeof device === 'number' || /^\d+$/.test(String(device)))
? parseInt(device, 10) : 0;
const audioFifo = `/tmp/dc-audio-${this._sessionIdForBridge}`;
// Create the audio FIFO before spawning the bridge.
const { execSync: _exec } = await import('child_process');
try { _exec(`mkfifo ${audioFifo}`); } catch (_) { /* may already exist */ }
const bridge = spawn('deltacast-capture', [
'--device', String(idx),
'--port', String(idx),
'--audio-pipe', audioFifo,
'--signal-timeout', '30',
], { stdio: ['ignore', 'pipe', 'pipe'] });
// Log bridge stderr after the first line (non-JSON diagnostic output)
let firstLineDone = false;
bridge.stderr.on('data', (d) => {
if (firstLineDone) console.error(`[deltacast-bridge] ${d}`);
else if (d.toString().includes('\n')) firstLineDone = true;
});
const fmt = await readFirstStderrLine(bridge, 35_000);
// fmt: { width, height, fps_num, fps_den, interlaced, pix_fmt,
// audio_channels, audio_rate, device, port }
return {
inputArgs: [
'-f', 'rawvideo',
'-pix_fmt', fmt.pix_fmt,
'-video_size', `${fmt.width}x${fmt.height}`,
'-framerate', `${fmt.fps_num}/${fmt.fps_den}`,
'-i', 'pipe:0',
'-f', 's16le',
'-ar', String(fmt.audio_rate),
'-ac', String(fmt.audio_channels),
'-i', audioFifo,
],
isNetwork: false,
bridgeProcess: bridge,
audioFifo,
interlaced: !!fmt.interlaced,
};
}
```
- [ ] **Step 2: Commit**
```bash
git add services/capture/src/capture-manager.js
git commit -m "feat(capture): replace deltacast _buildInputArgs stub with real bridge spawn"
```
---
## Task 6: capture-manager.js — `start()` bridge lifecycle + `stop()` cleanup
**Files:**
- Modify: `services/capture/src/capture-manager.js`
Four changes to `start()` and one to `stop()`.
- [ ] **Step 1: Store session ID before `_buildInputArgs` call**
In `start()`, before the `const { inputArgs, isNetwork } = await this._buildInputArgs(...)` call (currently around line 307), add:
```js
this._sessionIdForBridge = sessionId;
```
- [ ] **Step 2: Store bridge state after `_buildInputArgs` returns**
After `const { inputArgs, isNetwork } = await this._buildInputArgs(...)`, change the destructuring to also capture `bridgeProcess` and `audioFifo`:
```js
const { inputArgs, isNetwork, bridgeProcess = null, audioFifo = null, interlaced = false } = await this._buildInputArgs({
sourceType, device, sourceUrl, listen, listenPort, streamKey,
});
```
- [ ] **Step 3: Pipe bridge stdout into FFmpeg stdin for deltacast**
After `const hiresProcess = spawn('ffmpeg', hiresArgs, { stdio: hiresStdio });`, add:
```js
// For deltacast, the bridge writes raw video to its stdout.
// Pipe it into FFmpeg's stdin so FFmpeg reads -i pipe:0.
if (bridgeProcess) {
bridgeProcess.stdout.pipe(hiresProcess.stdin);
}
```
- [ ] **Step 4: Add bridge to `processes` map and `audioFifo` to `currentSession`**
Change the existing `const processes = { hires: hiresProcess };` line to:
```js
const processes = { hires: hiresProcess };
if (bridgeProcess) processes.bridge = bridgeProcess;
```
And in the `this.state.currentSession = { ... }` object (near the end of `start()`), add:
```js
audioFifo,
```
to the object literal (alongside `sourceType`, `device`, etc.).
- [ ] **Step 5: Fix deinterlace filter to include deltacast interlaced signals**
Find the line (currently ~321):
```js
const sdiFilterArgs = (sourceType === 'sdi') ? ['-vf', 'yadif=mode=1:deint=1'] : [];
```
Replace with:
```js
const isInterlacedSource = sourceType === 'sdi' || (sourceType === 'deltacast' && interlaced);
const sdiFilterArgs = isInterlacedSource ? ['-vf', 'yadif=mode=1:deint=1'] : [];
```
- [ ] **Step 6: Include deltacast in the HLS split-output branch**
Find the line (currently ~334):
```js
if (sourceType === 'sdi' && this._assetIdForHls) {
```
Replace with:
```js
if ((sourceType === 'sdi' || sourceType === 'deltacast') && this._assetIdForHls) {
```
- [ ] **Step 7: Kill bridge in `stop()` and clean up FIFO**
In the `stop()` method, find the existing kill block:
```js
if (processes.hires) processes.hires.kill('SIGINT');
if (processes.proxy) processes.proxy.kill('SIGINT');
if (processes.hls) { try { processes.hls.kill('SIGINT'); } catch (_) {} }
```
Add a bridge kill and FIFO cleanup:
```js
if (processes.hires) processes.hires.kill('SIGINT');
if (processes.proxy) processes.proxy.kill('SIGINT');
if (processes.hls) { try { processes.hls.kill('SIGINT'); } catch (_) {} }
if (processes.bridge) { try { processes.bridge.kill('SIGINT'); } catch (_) {} }
```
Then after the existing `await Promise.all(uploadPromises);` block (around line 462), add FIFO cleanup:
```js
if (currentSession.audioFifo) {
try { (await import('node:fs')).unlinkSync(currentSession.audioFifo); } catch (_) {}
}
```
- [ ] **Step 8: Commit**
```bash
git add services/capture/src/capture-manager.js
git commit -m "feat(capture): wire bridge process lifecycle into start/stop for deltacast"
```
---
## Task 7: routes/capture.js — Accept `deltacast` source_type
**Files:**
- Modify: `services/capture/src/routes/capture.js` (line 329)
- [ ] **Step 1: Find the source_type validation block in `/start` handler (around line 318)**
Current code:
```js
} else {
return res.status(400).json({
error: `Unknown source_type: ${source_type}. Must be sdi, srt, or rtmp`,
});
}
```
This `else` branch fires when source_type isn't `sdi`, `srt`, or `rtmp`. Add `deltacast` to the accepted list.
- [ ] **Step 2: Add deltacast validation before the else block**
After the `} else if (source_type === 'srt' || source_type === 'rtmp') {` block, add:
```js
} else if (source_type === 'deltacast') {
if (device === undefined || device === null) {
return res.status(400).json({ error: 'deltacast source requires: device (board/port index)' });
}
} else {
return res.status(400).json({
error: `Unknown source_type: ${source_type}. Must be sdi, srt, rtmp, or deltacast`,
});
}
```
- [ ] **Step 3: Commit**
```bash
git add services/capture/src/routes/capture.js
git commit -m "feat(capture): accept deltacast as valid source_type in /start handler"
```
---
## Task 8: Smoke test — verify the build and Node.js changes
**Files:** None created.
- [ ] **Step 1: Verify the bridge compiles on the capture host (or in Docker)**
On the Deltacast machine (once it is available), run:
```bash
cd services/capture
tar -xzf ../../videomaster-linux.x64-6.34.1-dev.tar.gz -C /tmp/sdk
cmake -S deltacast-bridge -B /tmp/bridge-build -DSDK_ROOT=/tmp/sdk -DCMAKE_BUILD_TYPE=Release
cmake --build /tmp/bridge-build -j$(nproc)
ls -lh /tmp/bridge-build/deltacast-capture
```
Expected: binary present, size ~50200KB.
Until the hardware machine is available, verify the CMakeLists.txt syntax is correct by running the configure step only:
```bash
cmake -S services/capture/deltacast-bridge -B /tmp/bridge-test \
-DSDK_ROOT=C:/Users/zacga/Nextcloud/Claude/Projects/Dragonflight \
--check-system-vars 2>&1 | head -20
```
- [ ] **Step 2: Verify capture-manager.js has no syntax errors**
```bash
cd services/capture
node --input-type=module < src/capture-manager.js 2>&1 | head -5
```
Expected: no output (file imports fine) or a module-not-found error for uuid (acceptable — the file is correct).
- [ ] **Step 3: Verify routes/capture.js has no syntax errors**
```bash
node --input-type=module < services/capture/src/routes/capture.js 2>&1 | head -5
```
Expected: no output or dependency error only.
- [ ] **Step 4: Confirm deltacast recorder creation is rejected correctly without device param**
Start the capture service locally (if possible) and POST:
```bash
curl -s -X POST http://localhost:3001/capture/start \
-H 'Content-Type: application/json' \
-d '{"project_id":"test","clip_name":"test","source_type":"deltacast"}' | jq .
```
Expected response:
```json
{"error":"deltacast source requires: device (board/port index)"}
```
- [ ] **Step 5: Final commit if any fixups were needed**
```bash
git add -A
git commit -m "fix(capture): deltacast smoke-test fixups"
```
---
## Hardware Validation Checklist (run on the Deltacast machine)
After the hardware machine is available:
1. Build the Docker image: `docker compose build capture`
2. Create a recorder with `source_type=deltacast`, `device=0`
3. Confirm capture container logs show the JSON format line within 5s of feed going live
4. Confirm recorder status shows `signal: "receiving"`
5. Record a 30s clip → verify asset created, proxy + HLS generated
6. Test stop mid-record → file finalized correctly
7. Test no-signal path → recorder stays idle, no asset created
8. Test container restart mid-record → existing asset finalized via `/finalize` endpoint

View file

@ -0,0 +1,231 @@
# Deltacast SDI Capture — Design Spec
**Date:** 2026-06-01
**Status:** Approved
**Approach:** Bridge binary (Option B2)
---
## Problem
Dragonflight supports SDI ingest via Blackmagic DeckLink. Deltacast VideoMaster cards are a second hardware target. The VideoMaster SDK (v6.34.1) ships C++ headers and shared libraries but no FFmpeg demuxer plugin — there is no mainline FFmpeg `-f deltacast` input device. The `capture-manager.js` stub exists but falls back to a lavfi test card on all deployments.
---
## Approach
Write a small C++ bridge binary (`deltacast-capture`) using the VideoMaster C++ Wrapper SDK. The bridge:
1. Detects signal format on startup, writes one JSON line to stderr
2. Streams raw YUV video frames to stdout
3. Streams raw PCM audio to a named FIFO
`capture-manager.js` reads the JSON handshake, then spawns FFmpeg with `-f rawvideo -i pipe:0` (video from bridge stdout) and `-f s16le -i <fifo>` (audio from FIFO). The existing HEVC NVENC / ProRes encode pipeline is unchanged.
---
## Architecture
```
┌─────────────────────────────────────────────────────────┐
│ capture container │
│ │
│ capture-manager.js │
│ │ │
│ ├─ spawn deltacast-capture --device 0 --port 0 │
│ │ --audio-pipe /tmp/dc-audio-{sessionId} │
│ │ │ │
│ │ ├─ stderr: JSON format line (one-time handshake) │
│ │ ├─ stdout: raw YUV frames (continuous) │
│ │ └─ FIFO: raw PCM audio (continuous) │
│ │ │
│ └─ spawn ffmpeg │
│ -f rawvideo -pix_fmt uyvy422 -s WxH -r FPS/1 │
│ -i pipe:0 ← piped from bridge stdout │
│ -f s16le -ar 48000 -ac <N>
│ -i /tmp/dc-audio-{sessionId} │
<hevc_nvenc / prores / h264 encode args>
<S3 pipe or growing-file output>
└─────────────────────────────────────────────────────────┘
```
### New files
- `services/capture/deltacast-bridge/CMakeLists.txt`
- `services/capture/deltacast-bridge/main.cpp`
### Modified files
- `services/capture/src/capture-manager.js``_buildInputArgs()` deltacast branch; `start()` and `stop()` bridge lifecycle
- `services/capture/Dockerfile` — SDK extraction stage, bridge build stage, runtime `.so` install
---
## The `deltacast-capture` Binary
### CLI
```
deltacast-capture
--device <N> Board index (0-based)
--port <N> RX port index (0-based)
--audio-pipe <path> Named FIFO path for PCM audio output
[--signal-timeout <sec=30>]
[--audio-groups <N=2>] Number of SDI audio groups (2 groups = 8 channels)
```
### Startup sequence
1. `Board::open(device, loopback_restore_cb)`
2. Disable loopback on `port`
3. `board.sdi().open_stream(rx_streamtype(port), VHD_SDI_STPROC_DISJOINED_VIDEO)`
4. Poll `wait_for_input()` up to `--signal-timeout` seconds
5. On timeout → write `{"error":"no signal","device":N,"port":N}` to stderr, exit 1
6. Detect `video_standard`, `clock_divisor`, `interface` → map to width/height/fps/pix_fmt/interlaced
7. Write one JSON line to stderr (flushed):
```json
{"width":1920,"height":1080,"fps_num":25,"fps_den":1,"pix_fmt":"uyvy422","interlaced":false,"audio_channels":8,"audio_rate":48000,"device":0,"port":0}
```
8. Set queue depth = 8, `rx_stream.start()`
9. Capture loop: `pop_slot()` → write video buffer to stdout → extract audio → write PCM to FIFO (background thread)
10. SIGTERM/SIGINT → set stop flag → flush, close FIFO, close stream/board, exit 0
### Pixel format
Default: `uyvy422` (4:2:2 8-bit, `VHD_SDI_BUFTYPE_VIDEO`). 10-bit (`v210`) is a future follow-up via `--pix-fmt v210`.
### Audio
`sdi_slot.audio().extract(num_groups)` returns `std::vector<VHD_AUDIOGROUP>`. Samples are written to the FIFO as interleaved s16le PCM at 48000 Hz in a background thread so the video loop never blocks on audio consumers. Default `--audio-groups 2` yields 8 channels (standard embedded SDI stereo pairs 14).
---
## `capture-manager.js` Changes
### `_buildInputArgs()` — deltacast branch
Replace the existing lavfi-fallback stub with:
```js
if (sourceType === 'deltacast') {
const idx = parseInt(device, 10) || 0;
const audioFifo = `/tmp/dc-audio-${sessionId}`;
await execAsync(`mkfifo ${audioFifo}`);
const bridge = spawn('deltacast-capture', [
'--device', String(idx),
'--port', String(idx), // port == board index for single-port-per-recorder model
'--audio-pipe', audioFifo,
], { stdio: ['ignore', 'pipe', 'pipe'] });
const fmt = await readFirstStderrLine(bridge, 35_000); // 35s timeout
// fmt: { width, height, fps_num, fps_den, pix_fmt, interlaced, audio_channels, audio_rate }
return {
inputArgs: [
'-f', 'rawvideo',
'-pix_fmt', fmt.pix_fmt,
'-video_size', `${fmt.width}x${fmt.height}`,
'-framerate', `${fmt.fps_num}/${fmt.fps_den}`,
'-i', 'pipe:0',
'-f', 's16le',
'-ar', String(fmt.audio_rate),
'-ac', String(fmt.audio_channels),
'-i', audioFifo,
],
isNetwork: false,
bridgeProcess: bridge,
audioFifo,
interlaced: fmt.interlaced,
};
}
```
`readFirstStderrLine(proc, timeoutMs)` is a small helper that returns a parsed JSON object from the first line emitted on `proc.stderr`, or throws on timeout or non-zero exit.
### `start()` changes
- After `_buildInputArgs()` returns, store `bridgeProcess` and `audioFifo` on `this.state`
- Spawn FFmpeg with `stdio: ['pipe', ...]` for stdin
- `bridgeProcess.stdout.pipe(hiresProcess.stdin)`
- Deinterlace: if `interlaced === true`, add `-vf yadif=mode=1:deint=1` (already present for `sourceType === 'sdi'`; extend that check to include `deltacast`)
### `stop()` changes
- `if (processes.bridge) processes.bridge.kill('SIGINT')`
- After process cleanup: `if (this.state.audioFifo) { try { fs.unlinkSync(this.state.audioFifo); } catch (_) {} }`
### HLS preview
The existing `filter_complex split` SDI preview path works unchanged — the bridge→pipe is just a different `-i` source. Extend the `sourceType === 'sdi'` guard to `['sdi', 'deltacast'].includes(sourceType)`.
---
## Dockerfile Changes
```dockerfile
# ── Stage 0: Extract VideoMaster SDK ─────────────────────────────────────
FROM debian:bookworm AS sdk-extractor
COPY videomaster-linux.x64-6.34.1-dev.tar.gz /tmp/
RUN mkdir -p /sdk && tar -xzf /tmp/videomaster-linux.x64-6.34.1-dev.tar.gz -C /sdk
# ── Stage 1: Build deltacast-capture bridge ───────────────────────────────
FROM debian:bookworm AS bridge-builder
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential cmake ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY --from=sdk-extractor /sdk /sdk
COPY deltacast-bridge/ /bridge/
RUN cmake -S /bridge -B /bridge/build \
-DCMAKE_BUILD_TYPE=Release \
-DSDK_ROOT=/sdk \
&& cmake --build /bridge/build -j$(nproc)
# ── Stage 2: Build FFmpeg (unchanged) ─────────────────────────────────────
FROM debian:bookworm AS ffmpeg-builder
# ... existing content, no changes ...
# ── Stage 3: Runtime ──────────────────────────────────────────────────────
FROM node:20-bookworm
# ... existing runtime deps ...
COPY --from=bridge-builder /bridge/build/deltacast-capture /usr/local/bin/deltacast-capture
COPY --from=sdk-extractor /sdk/lib/ /usr/local/lib/deltacast/
RUN ldconfig /usr/local/lib/deltacast && ldconfig
```
SDK `.so` files total ~4MB. The bridge binary adds ~200KB.
---
## Error Handling
| Scenario | Bridge behavior | `capture-manager.js` response |
|---|---|---|
| No signal within timeout | Exit 1, `{"error":"no signal"}` on stderr | Throws — recorder stays idle, no asset created |
| Invalid board/port | Exit 1, `{"error":"board N not found"}` | Same as above |
| Bridge crash mid-capture | stdout closes → FFmpeg stdin EOF → FFmpeg exits cleanly | Existing stop handler fires; asset finalized with frames received so far |
| Audio FIFO open stall | Bridge blocks on FIFO write-open until FFmpeg opens read-end | Guarded by 10s watchdog on bridge spawn; if FFmpeg fails to start, bridge is SIGKILL'd |
| FIFO leftover on container crash | Stale file in `/tmp/` | Next `start()` uses a new `sessionId`-based path; harmless |
---
## Testing
### Without hardware (dev mode)
The lavfi fallback is **removed** from the deltacast branch — a missing `deltacast-capture` binary will throw at spawn time (clear error). Developers run the existing test card by using `sourceType = 'sdi'` with a DeckLink card or `sourceType = 'srt'` with a test stream.
The bridge binary can be tested standalone:
```bash
mkfifo /tmp/test-audio
deltacast-capture --device 0 --port 0 --audio-pipe /tmp/test-audio &
# watch stderr for JSON line, then:
cat /tmp/test-audio | ffprobe -f s16le -ar 48000 -ac 8 -i -
```
### With hardware (post-implementation)
1. Create recorder: `source_type=deltacast`, `device=0`, `port=0`
2. Verify JSON handshake in capture container logs within signal timeout
3. Verify `signal=receiving` in recorder status
4. Record 30s clip → asset created, proxy + HLS generated
5. Test stop mid-record → file finalized correctly
6. Test no-signal → recorder stays idle, no asset created
7. Test container restart mid-record → asset finalized on restart via existing `finalize` endpoint
---
## Out of Scope
- 10-bit (`v210`) pixel format — follow-up
- `--audio-groups` UI control — follow-up
- GPU extension SDK (`gpuextension-linux.x64-2.2.0-dev.zip`) — covers GPU-accelerated colorspace conversion on the card; not needed for basic capture
- IP virtual card SDK (`ipvirtualcard`) — separate feature
- Promoting bridge to a native FFmpeg `libavdevice` input device — future v2

42
sdk/README.md Normal file
View file

@ -0,0 +1,42 @@
# Capture-card SDK / driver file store
This directory holds the **proprietary, non-redistributable** vendor SDKs and
drivers used to enable SDI / NDI capture cards on cluster nodes.
> **INTERNAL ONLY.** These files are licensed by their respective vendors and
> must **not** be published, redistributed, or committed to any public mirror.
> This repository is private. Do not change that.
## Why these live in the repo
The cluster admin screen lets an operator install/update capture-card drivers on
a node from the web UI (no SSH). The node-agent spawns a one-shot privileged
container that bind-mounts this repository and runs
[`deploy/install-driver.sh <vendor>`](../deploy/install-driver.sh), which reads
the vendor files from `sdk/<vendor>/`. Because the install must work offline on
an isolated broadcast LAN, the binaries ship in-repo rather than being fetched at
install time.
## Layout
```
sdk/
README.md ← this file
blackmagic/ ← Blackmagic Desktop Video (DeckLink) .deb
aja/ ← AJA ntv2 driver source / installer
deltacast/ ← Deltacast VideoMaster installer
ndi/ ← NDI redistributable runtime libs
```
Each vendor directory has its own `README.md` listing **exactly** which files an
admin must drop in. A `.gitkeep` keeps the empty directory committed.
## Important
- **No binaries are committed by default.** The directory structure + READMEs
are the deliverable. An admin downloads the proprietary files from the vendor
(per their licence) and drops them in the matching `sdk/<vendor>/` directory.
- The install script **fails gracefully** with a clear message if the expected
file is absent — it never fabricates or downloads binaries.
- Target host OS for all install paths is **Ubuntu 22.04 LTS (jammy), x86_64**,
matching the cluster worker nodes.

0
sdk/aja/.gitkeep Normal file
View file

31
sdk/aja/README.md Normal file
View file

@ -0,0 +1,31 @@
# AJA NTV2 driver
Drop the **AJA NTV2** driver source/SDK archive for Linux into this directory.
## Required file
| File | Notes |
|------|-------|
| `ntv2sdk*linux*.zip` **or** `libajantv2*.tar.gz` | The NTV2 SDK / open-source `libajantv2` source tree containing `driver/linux/` with the kernel-module `Makefile` and `load_ajantv2` / `unload_ajantv2` scripts. |
Example names: `ntv2sdklinux_17.0.1.zip`, `libajantv2-17.0.1.tar.gz`
The installer reads the **newest** matching archive.
## Where to get it
AJA → Support → Software & firmware → *NTV2 SDK* (Linux), or the public
`aja-video/libajantv2` source release. Download the Linux SDK zip / source
tarball and copy it here unmodified.
## What the install script does
1. Ensures `linux-headers-$(uname -r)`, `build-essential` are present.
2. Extracts the archive into a scratch build dir.
3. Builds the `ajantv2` kernel module from `driver/linux` (`make`).
4. Installs the module under `/lib/modules/$(uname -r)/extra`, runs `depmod`,
`modprobe ajantv2` (falls back to the SDK's `load_ajantv2` script).
5. Verifies the `ajantv2` module is loaded.
A **reboot is not normally required**; the module loads immediately after build.
The script reports if a reboot is needed (e.g. an old in-tree module is wedged).

0
sdk/blackmagic/.gitkeep Normal file
View file

35
sdk/blackmagic/README.md Normal file
View file

@ -0,0 +1,35 @@
# Blackmagic Desktop Video (DeckLink) driver
Drop the **Blackmagic Desktop Video** Debian package for **Ubuntu 22.04 (x86_64)**
into this directory.
## Required file
| File | Notes |
|------|-------|
| `desktopvideo_*_amd64.deb` | The `desktopvideo` package from the Desktop Video installer archive. Provides the `blackmagic` kernel module (built via DKMS) and the `DesktopVideoHelper` daemon. |
Example name: `desktopvideo_14.4.1a4_amd64.deb`
The installer reads the **newest** matching `desktopvideo_*_amd64.deb` if more
than one is present.
## Where to get it
Blackmagic Design → Support → *Desktop Video* (Linux). Download the
"Desktop Video x.y.z Linux" tarball, extract it, and copy the
`deb/<arch>/desktopvideo_*_amd64.deb` file here.
> Optional: `desktopvideo-gui_*_amd64.deb` is **not** required for headless
> capture and is not installed.
## What the install script does
1. Ensures `linux-headers-$(uname -r)` is present (needed for the DKMS build).
2. `apt-get install -y ./desktopvideo_*_amd64.deb` (pulls DKMS deps).
3. Triggers the DKMS build, `depmod`, `modprobe blackmagic`.
4. Restarts the `DesktopVideoHelper` daemon.
5. Verifies `/dev/blackmagic` appears.
A **reboot is usually not required** but a DKMS rebuild against a freshly
installed kernel may need one — the script reports this.

0
sdk/deltacast/.gitkeep Normal file
View file

31
sdk/deltacast/README.md Normal file
View file

@ -0,0 +1,31 @@
# Deltacast VideoMaster driver / SDK
Drop the **Deltacast VideoMaster** Linux installer into this directory.
## Required file
| File | Notes |
|------|-------|
| `VideoMaster*.run` **or** `VideoMaster*linux*.tar.gz` | The VideoMaster SDK + driver installer for Linux. Contains the `videomasterhd` kernel module sources and the `install.sh` driver installer. |
Example names: `VideoMaster-6.25.0.run`, `VideoMaster_6_25_Linux.tar.gz`
The installer reads the **newest** matching file.
## Where to get it
Deltacast → Products → *SDK* (<https://www.deltacast.tv/products/sdk>). Request
the VideoMaster Linux package (licence-gated) and copy the `.run` self-extractor
or the `.tar.gz` here unmodified.
## What the install script does
1. Ensures `linux-headers-$(uname -r)`, `build-essential`, `dkms` are present.
2. Runs the vendor installer:
- `.run` → executed with `--noexec --target <dir>` then its `install.sh`,
- `.tar.gz` → extracted, then its bundled `install.sh` is run.
3. Loads the Deltacast module (`modprobe videomasterhd` / vendor load script).
4. Verifies a `/dev/deltacast*` device node appears.
A **reboot may be required** after a first-time VideoMaster install (udev rules
+ firmware). The script reports this explicitly.

0
sdk/ndi/.gitkeep Normal file
View file

35
sdk/ndi/README.md Normal file
View file

@ -0,0 +1,35 @@
# NDI redistributable runtime
Drop the **NDI runtime redistributable** shared libraries into this directory.
NDI has **no kernel module** — it is purely user-space shared libraries, so this
is the lowest-risk install (no DKMS, no reboot).
## Required files
| File | Notes |
|------|-------|
| `libndi.so.*` | The versioned NDI runtime shared object, e.g. `libndi.so.6`. **Required.** |
| `libndi.so` *(optional)* | Dev symlink. The installer recreates it if absent. |
You may instead drop the whole **NDI SDK / Advanced SDK** `lib/x86_64-linux-gnu/`
directory contents here; the installer copies every `libndi*.so*` it finds.
Example name: `libndi.so.6.1.1`
## Where to get it
NDI → Tools / SDK download (NDI 6 SDK or NDI Advanced SDK for Linux). The
runtime libs live under `lib/x86_64-linux-gnu/` in the SDK. Per the NDI licence
the runtime is redistributable **within your own product** only — keep it in this
private repo, do not publish it.
## What the install script does
1. Copies every `libndi*.so*` from here into `/opt/ndi-lib`.
2. Writes `/etc/ld.so.conf.d/ndi.conf` pointing at `/opt/ndi-lib` and runs
`ldconfig`.
3. Recreates the `libndi.so``libndi.so.<N>` dev symlink if missing.
4. Verifies `ldconfig -p | grep libndi` resolves.
**No reboot required.** Running processes that already loaded an old `libndi`
must be restarted to pick up the new version — the script notes this.

View file

@ -1,4 +1,21 @@
# ── Stage 1: Build FFmpeg with DeckLink + NVENC (HEVC/H264) support ───────── # ── Stage 0: Extract Deltacast VideoMaster SDK ───────────────────────────
FROM debian:bookworm AS sdk-extractor
COPY videomaster-linux.x64-6.34.1-dev.tar.gz /tmp/
RUN mkdir -p /sdk && tar -xzf /tmp/videomaster-linux.x64-6.34.1-dev.tar.gz -C /sdk
# ── Stage 1: Build deltacast-capture bridge binary ───────────────────────
FROM debian:bookworm AS bridge-builder
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential cmake ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY --from=sdk-extractor /sdk /sdk
COPY deltacast-bridge/ /bridge/
RUN cmake -S /bridge -B /bridge/build \
-DCMAKE_BUILD_TYPE=Release \
-DSDK_ROOT=/sdk \
&& cmake --build /bridge/build -j$(nproc)
# ── Stage 2: Build FFmpeg with DeckLink + NVENC (HEVC/H264) support ─────────
# All-Intra HEVC NVENC is the master codec for growing-file ingest (see # All-Intra HEVC NVENC is the master codec for growing-file ingest (see
# docs/design/2026-05-29-all-intra-hevc-ingest.md). This stage gets the # docs/design/2026-05-29-all-intra-hevc-ingest.md). This stage gets the
# nv-codec-headers (header-only, no driver / no full CUDA toolkit needed) # nv-codec-headers (header-only, no driver / no full CUDA toolkit needed)
@ -64,6 +81,34 @@ RUN ./configure \
RUN /usr/local/bin/ffmpeg -hide_banner -encoders 2>&1 | grep -E 'nvenc' \ RUN /usr/local/bin/ffmpeg -hide_banner -encoders 2>&1 | grep -E 'nvenc' \
|| (echo 'FATAL: nvenc encoders missing from ffmpeg build' && exit 1) || (echo 'FATAL: nvenc encoders missing from ffmpeg build' && exit 1)
# ── Stage 1b: Build bmx (raw2bmx / bmxtranswrap) from source ─────────────────
# bmx (bmxlib + libMXF + libMXF++) is the reference GROWING OP1a MXF writer. It
# writes a fresh IndexTableSegment (with an updated IndexDuration) into a new
# body partition at a periodic interval, so the recorded duration is readable —
# and INCREASES — from the header+index alone while the file is still being
# written (no footer needed). This is what makes the master a TRUE Premiere
# growing file. ffmpeg's MXF muxer cannot do this (its real duration/index lands
# only in the footer at av_write_trailer, so duration probes N/A until close).
#
# Debian/Ubuntu have no `bmxlib-tools` package (verified absent in bookworm), so
# we build from the BBC source. liburiparser/uuid/lzma/zlib/expat are the build
# deps; the runtime needs only libexpat1 + liburiparser1 + libuuid1 (added in
# the runtime stage below). Pinned to the bbc/bmx default branch (v1.6.x).
FROM debian:bookworm AS bmx-builder
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential cmake git ca-certificates pkg-config \
liburiparser-dev uuid-dev liblzma-dev zlib1g-dev libexpat1-dev \
&& rm -rf /var/lib/apt/lists/*
# Pin to a release tag so the produced soname (libMXF.so.1.6 etc.) stays stable
# for the COPY in the runtime stage. v1.6 is the BBC bmx series verified here.
RUN git clone --recursive --branch v1.6 https://github.com/bbc/bmx.git /bmx \
|| git clone --recursive https://github.com/bbc/bmx.git /bmx
WORKDIR /bmx/build
RUN cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/local .. \
&& make -j"$(nproc)" && make install && ldconfig
# Sanity-check: raw2bmx must run, otherwise the growing-MXF pipeline is broken.
RUN /usr/local/bin/raw2bmx -h >/dev/null 2>&1 && echo 'raw2bmx OK'
# ── Stage 2: Runtime image ─────────────────────────────────────────────────── # ── Stage 2: Runtime image ───────────────────────────────────────────────────
FROM node:20-bookworm FROM node:20-bookworm
@ -75,6 +120,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
libx264-164 libx265-199 libvpx7 libopus0 libmp3lame0 \ libx264-164 libx265-199 libvpx7 libopus0 libmp3lame0 \
libsrt1.5-openssl libzmq5 libstdc++6 libc++1 libc++abi1 \ libsrt1.5-openssl libzmq5 libstdc++6 libc++1 libc++abi1 \
cifs-utils util-linux \ cifs-utils util-linux \
libexpat1 liburiparser1 libuuid1 \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Copy compiled ffmpeg/ffprobe # Copy compiled ffmpeg/ffprobe
@ -85,7 +131,34 @@ COPY --from=ffmpeg-builder /usr/local/lib/ /usr/local/lib/
# DeckLink runtime .so # DeckLink runtime .so
COPY lib/libDeckLinkAPI.so /usr/lib/libDeckLinkAPI.so COPY lib/libDeckLinkAPI.so /usr/lib/libDeckLinkAPI.so
COPY lib/libDeckLinkPreviewAPI.so /usr/lib/libDeckLinkPreviewAPI.so COPY lib/libDeckLinkPreviewAPI.so /usr/lib/libDeckLinkPreviewAPI.so
RUN ldconfig
# bmx (raw2bmx / bmxtranswrap / mxf2raw) — the growing OP1a MXF writer used for
# the edit-while-record master. Copy the built binaries + shared libs; runtime
# deps (libexpat1/liburiparser1/libuuid1) were installed above.
COPY --from=bmx-builder /usr/local/bin/raw2bmx /usr/local/bin/raw2bmx
COPY --from=bmx-builder /usr/local/bin/bmxtranswrap /usr/local/bin/bmxtranswrap
COPY --from=bmx-builder /usr/local/bin/mxf2raw /usr/local/bin/mxf2raw
COPY --from=bmx-builder /usr/local/lib/libMXF.so.1.6 /usr/local/lib/
COPY --from=bmx-builder /usr/local/lib/libMXF++.so.1.6 /usr/local/lib/
COPY --from=bmx-builder /usr/local/lib/libbmx.so.1.6 /usr/local/lib/
RUN cd /usr/local/lib \
&& ln -sf libMXF.so.1.6 libMXF.so.1 && ln -sf libMXF.so.1 libMXF.so \
&& ln -sf libMXF++.so.1.6 libMXF++.so.1 && ln -sf libMXF++.so.1 libMXF++.so \
&& ln -sf libbmx.so.1.6 libbmx.so.1 && ln -sf libbmx.so.1 libbmx.so \
&& ldconfig
# Verify raw2bmx resolves its libs and runs in the final image.
RUN raw2bmx -h >/dev/null 2>&1 && echo 'raw2bmx runtime OK'
# Deltacast bridge binary + SDK runtime libs
COPY --from=bridge-builder /bridge/build/deltacast-capture /usr/local/bin/deltacast-capture
COPY --from=sdk-extractor /sdk/lib/libvideomasterhd.so.6.34.1 /usr/local/lib/deltacast/
COPY --from=sdk-extractor /sdk/lib/libvideomasterhd_audio.so.6.34.1 /usr/local/lib/deltacast/
RUN ln -sf libvideomasterhd.so.6.34.1 /usr/local/lib/deltacast/libvideomasterhd.so.6 \
&& ln -sf libvideomasterhd.so.6.34.1 /usr/local/lib/deltacast/libvideomasterhd.so \
&& ln -sf libvideomasterhd_audio.so.6.34.1 /usr/local/lib/deltacast/libvideomasterhd_audio.so.6 \
&& ln -sf libvideomasterhd_audio.so.6.34.1 /usr/local/lib/deltacast/libvideomasterhd_audio.so \
&& ldconfig /usr/local/lib/deltacast \
&& ldconfig
# Mount points the recorder lifecycle expects to exist. # Mount points the recorder lifecycle expects to exist.
# /live — HLS preview output (bound from host LIVE_DIR by node-agent) # /live — HLS preview output (bound from host LIVE_DIR by node-agent)

View file

@ -0,0 +1,37 @@
cmake_minimum_required(VERSION 3.16)
project(deltacast-bridge C)
set(CMAKE_C_STANDARD 17)
set(SDK_ROOT "/sdk" CACHE PATH "Path to extracted VideoMaster SDK")
# Primary binary: deltacast-bridge (shared multi-port daemon)
add_executable(deltacast-bridge main.c)
target_include_directories(deltacast-bridge PRIVATE
${SDK_ROOT}/include/videomaster
)
target_link_directories(deltacast-bridge PRIVATE
${SDK_ROOT}/lib
)
target_link_libraries(deltacast-bridge PRIVATE
videomasterhd
videomasterhd_audio
pthread
)
# Embed the SDK RPATH so the binary finds the .so at runtime
set_target_properties(deltacast-bridge PROPERTIES
INSTALL_RPATH "/usr/local/lib/deltacast"
BUILD_WITH_INSTALL_RPATH TRUE
)
# Compat symlink: deltacast-capture -> deltacast-bridge
# (node-agent and any legacy scripts that reference the old name still work)
add_custom_command(TARGET deltacast-bridge POST_BUILD
COMMAND ${CMAKE_COMMAND} -E create_symlink
$<TARGET_FILE:deltacast-bridge>
$<TARGET_FILE_DIR:deltacast-bridge>/deltacast-capture
COMMENT "Creating deltacast-capture compat symlink"
)

View file

@ -0,0 +1,525 @@
/* services/capture/deltacast-bridge/main.c
*
* Deltacast VideoMaster SDI shared multi-port bridge daemon.
*
* Opens the board ONCE, opens RX streams for all requested ports, and
* writes each port's video/audio to named FIFOs in a shared directory.
* One reader thread + one audio thread per port run concurrently.
*
* Usage:
* deltacast-bridge --device <N> --ports <csv>
* [--video-pipe-dir /dev/shm/deltacast]
* [--audio-pipe-dir /dev/shm/deltacast]
* [--signal-timeout <sec>]
*
* Compat alias: --port <N> treated as --ports <N> (single port).
*
* For each port that acquires signal, emits one JSON line to stderr:
* {"port":N,"width":W,"height":H,"fps_num":N,"fps_den":D,
* "pix_fmt":"uyvy422","audio_rate":48000,"audio_channels":2}
*
* Runs until SIGTERM/SIGINT, then closes all streams and the board.
*/
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <signal.h>
#include <stdatomic.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
#include "VideoMasterHD_Core.h"
#include "VideoMasterHD_Sdi.h"
#include "VideoMasterHD_Sdi_Audio.h"
/* ── Globals ──────────────────────────────────────────────────────────── */
static atomic_int g_stop = 0;
static void on_signal(int s) { (void)s; atomic_store(&g_stop, 1); }
/* ── Constants ────────────────────────────────────────────────────────── */
#define MAX_PORTS 8
/* ── Stream type by port index (non-contiguous SDK enum) ────────────── */
static ULONG rx_streamtype(unsigned port) {
switch (port) {
case 0: return VHD_ST_RX0;
case 1: return VHD_ST_RX1;
case 2: return VHD_ST_RX2;
case 3: return VHD_ST_RX3;
case 4: return VHD_ST_RX4;
case 5: return VHD_ST_RX5;
case 6: return VHD_ST_RX6;
case 7: return VHD_ST_RX7;
default:
fprintf(stderr, "{\"error\":\"port %u not supported (max 7)\"}\n", port);
return VHD_ST_RX0;
}
}
/* ── Loopback board property by port index ───────────────────────────── */
static ULONG loopback_prop(unsigned port) {
switch (port) {
case 0: return VHD_CORE_BP_PASSIVE_LOOPBACK_0;
case 1: return VHD_CORE_BP_PASSIVE_LOOPBACK_1;
case 2: return VHD_CORE_BP_PASSIVE_LOOPBACK_2;
case 3: return VHD_CORE_BP_PASSIVE_LOOPBACK_3;
default: return VHD_CORE_BP_PASSIVE_LOOPBACK_0;
}
}
/* ── Video standard → width/height/fps/interlaced ───────────────────── */
typedef struct { int width, height, fps_num, fps_den; int interlaced; } VideoInfo;
static VideoInfo video_info(VHD_VIDEOSTANDARD std, VHD_CLOCKDIVISOR div) {
int ntsc = (div == VHD_CLOCKDIV_1001);
switch (std) {
case VHD_VIDEOSTD_S274M_1080p_25Hz: return (VideoInfo){1920,1080,25,1,0};
case VHD_VIDEOSTD_S274M_1080p_30Hz: return (VideoInfo){1920,1080,ntsc?30000:30,ntsc?1001:1,0};
case VHD_VIDEOSTD_S274M_1080p_24Hz: return (VideoInfo){1920,1080,ntsc?24000:24,ntsc?1001:1,0};
case VHD_VIDEOSTD_S274M_1080p_50Hz: return (VideoInfo){1920,1080,50,1,0};
case VHD_VIDEOSTD_S274M_1080p_60Hz: return (VideoInfo){1920,1080,ntsc?60000:60,ntsc?1001:1,0};
case VHD_VIDEOSTD_S274M_1080psf_24Hz: return (VideoInfo){1920,1080,ntsc?24000:24,ntsc?1001:1,0};
case VHD_VIDEOSTD_S274M_1080psf_25Hz: return (VideoInfo){1920,1080,25,1,0};
case VHD_VIDEOSTD_S274M_1080psf_30Hz: return (VideoInfo){1920,1080,ntsc?30000:30,ntsc?1001:1,0};
case VHD_VIDEOSTD_S274M_1080i_50Hz: return (VideoInfo){1920,1080,25,1,1};
case VHD_VIDEOSTD_S274M_1080i_60Hz: return (VideoInfo){1920,1080,ntsc?30000:30,ntsc?1001:1,1};
case VHD_VIDEOSTD_S296M_720p_50Hz: return (VideoInfo){1280,720,50,1,0};
case VHD_VIDEOSTD_S296M_720p_60Hz: return (VideoInfo){1280,720,ntsc?60000:60,ntsc?1001:1,0};
case VHD_VIDEOSTD_S296M_720p_25Hz: return (VideoInfo){1280,720,25,1,0};
case VHD_VIDEOSTD_S296M_720p_30Hz: return (VideoInfo){1280,720,ntsc?30000:30,ntsc?1001:1,0};
case VHD_VIDEOSTD_S296M_720p_24Hz: return (VideoInfo){1280,720,ntsc?24000:24,ntsc?1001:1,0};
case VHD_VIDEOSTD_3840x2160p_24Hz: return (VideoInfo){3840,2160,ntsc?24000:24,ntsc?1001:1,0};
case VHD_VIDEOSTD_3840x2160p_25Hz: return (VideoInfo){3840,2160,25,1,0};
case VHD_VIDEOSTD_3840x2160p_30Hz: return (VideoInfo){3840,2160,ntsc?30000:30,ntsc?1001:1,0};
case VHD_VIDEOSTD_3840x2160p_50Hz: return (VideoInfo){3840,2160,50,1,0};
case VHD_VIDEOSTD_3840x2160p_60Hz: return (VideoInfo){3840,2160,ntsc?60000:60,ntsc?1001:1,0};
case VHD_VIDEOSTD_S259M_NTSC_480: return (VideoInfo){720,480,ntsc?30000:30,ntsc?1001:1,1};
default: return (VideoInfo){1920,1080,25,1,0};
}
}
/* ── Write-all helper ─────────────────────────────────────────────────── */
static int write_all(int fd, const unsigned char *p, size_t len) {
size_t off = 0;
while (off < len) {
ssize_t n = write(fd, p + off, len - off);
if (n > 0) { off += (size_t)n; continue; }
if (n < 0 && errno == EINTR) continue;
return -1;
}
return 0;
}
/* ── Per-port state ───────────────────────────────────────────────────── */
typedef struct {
HANDLE board;
unsigned port;
unsigned device;
ULONG video_std;
ULONG clock_div;
VideoInfo vi;
char video_fifo[256];
char audio_fifo[256];
/* threads */
pthread_t video_tid;
pthread_t audio_tid;
/* streams (owned by threads, set before thread launch) */
HANDLE video_stream;
} PortState;
/* ── Audio thread ──────────────────────────────────────────────────────
*
* Identical design to the single-port bridge audio thread:
* - Opens FIFO writer FIRST, unconditionally (unblocks ffmpeg input)
* - Feeds continuous wall-clock-paced s16le stereo (real or silence)
* - Best-effort VHD audio stream; silence fallback on any failure
*/
static void *audio_thread(void *arg) {
PortState *ps = (PortState *)arg;
int fd = open(ps->audio_fifo, O_WRONLY);
if (fd < 0) {
fprintf(stderr, "[audio:%u] open FIFO failed: %s\n", ps->port, strerror(errno));
return NULL;
}
const int AUDIO_RATE = 48000;
const int CHANNELS = 2;
const size_t FRAME_BYTES = (size_t)CHANNELS * 2; /* s16le stereo */
int fps_num = ps->vi.fps_num > 0 ? ps->vi.fps_num : 25;
int fps_den = ps->vi.fps_den > 0 ? ps->vi.fps_den : 1;
long samples_per_frame = ((long)AUDIO_RATE * fps_den + fps_num / 2) / fps_num;
if (samples_per_frame < 1) samples_per_frame = 1;
size_t tick_bytes = (size_t)samples_per_frame * FRAME_BYTES;
ULONG max_samples = VHD_GetNbSamples((VHD_VIDEOSTANDARD)ps->video_std,
(VHD_CLOCKDIVISOR)ps->clock_div,
VHD_ASR_48000, 0);
ULONG block_size = VHD_GetBlockSize(VHD_AF_16, VHD_AM_STEREO);
size_t vhd_buf_sz = ((size_t)max_samples + 64) * (block_size ? block_size : FRAME_BYTES);
size_t buf_sz = vhd_buf_sz > tick_bytes ? vhd_buf_sz : tick_bytes;
unsigned char *buf = calloc(1, buf_sz);
if (!buf) { close(fd); return NULL; }
HANDLE stream = NULL;
int have_vhd_audio = 0;
VHD_AUDIOINFO ai;
memset(&ai, 0, sizeof(ai));
ULONG r = VHD_OpenStreamHandle(ps->board, rx_streamtype(ps->port),
VHD_SDI_STPROC_DISJOINED_ANC,
NULL, &stream, NULL);
if (r == VHDERR_NOERROR) {
VHD_SetStreamProperty(stream, VHD_SDI_SP_VIDEO_STANDARD, ps->video_std);
VHD_SetStreamProperty(stream, VHD_SDI_SP_CLOCK_SYSTEM, ps->clock_div);
VHD_SetStreamProperty(stream, VHD_CORE_SP_TRANSFER_SCHEME, VHD_TRANSFER_SLAVED);
ai.pAudioGroups[0].pAudioChannels[0].Mode = VHD_AM_STEREO;
ai.pAudioGroups[0].pAudioChannels[0].BufferFormat = VHD_AF_16;
ai.pAudioGroups[0].pAudioChannels[0].pData = buf;
if (VHD_StartStream(stream) == VHDERR_NOERROR) {
have_vhd_audio = 1;
} else {
fprintf(stderr, "[audio:%u] VHD_StartStream failed — feeding silence\n", ps->port);
VHD_CloseStreamHandle(stream);
stream = NULL;
}
} else {
fprintf(stderr, "[audio:%u] VHD_OpenStreamHandle failed (%lu) — feeding silence\n",
ps->port, r);
}
struct timespec next;
clock_gettime(CLOCK_MONOTONIC, &next);
long frame_ns = (long)(1000000000.0 * (double)fps_den / (double)fps_num);
HANDLE slot = NULL;
while (!atomic_load(&g_stop)) {
size_t out_bytes = 0;
if (have_vhd_audio) {
r = VHD_LockSlotHandle(stream, &slot);
if (r == VHDERR_NOERROR) {
ai.pAudioGroups[0].pAudioChannels[0].DataSize = (ULONG)buf_sz;
if (VHD_SlotExtractAudio(slot, &ai) == VHDERR_NOERROR) {
ULONG sz = ai.pAudioGroups[0].pAudioChannels[0].DataSize;
if (sz > 0 && (size_t)sz <= buf_sz) out_bytes = (size_t)sz;
}
VHD_UnlockSlotHandle(slot);
} else if (r != VHDERR_TIMEOUT) {
fprintf(stderr, "[audio:%u] lock error %lu — degrading to silence\n",
ps->port, r);
VHD_StopStream(stream);
VHD_CloseStreamHandle(stream);
stream = NULL;
have_vhd_audio = 0;
}
}
if (out_bytes == 0) {
memset(buf, 0, tick_bytes);
out_bytes = tick_bytes;
}
if (write_all(fd, buf, out_bytes) < 0) {
atomic_store(&g_stop, 1);
break;
}
next.tv_nsec += frame_ns;
while (next.tv_nsec >= 1000000000L) { next.tv_nsec -= 1000000000L; next.tv_sec += 1; }
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
if (next.tv_sec > now.tv_sec ||
(next.tv_sec == now.tv_sec && next.tv_nsec > now.tv_nsec)) {
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next, NULL);
} else {
next = now;
}
}
close(fd);
if (stream) {
VHD_StopStream(stream);
VHD_CloseStreamHandle(stream);
}
free(buf);
return NULL;
}
/* ── Video thread ─────────────────────────────────────────────────────── */
static void *video_thread(void *arg) {
PortState *ps = (PortState *)arg;
int fd = open(ps->video_fifo, O_WRONLY);
if (fd < 0) {
fprintf(stderr, "[video:%u] open FIFO failed: %s\n", ps->port, strerror(errno));
return NULL;
}
HANDLE slot = NULL;
while (!atomic_load(&g_stop)) {
ULONG r = VHD_LockSlotHandle(ps->video_stream, &slot);
if (r == VHDERR_NOERROR) {
BYTE *buf = NULL;
ULONG sz = 0;
if (VHD_GetSlotBuffer(slot, VHD_SDI_BT_VIDEO, &buf, &sz) == VHDERR_NOERROR) {
if (write_all(fd, buf, sz) < 0) {
atomic_store(&g_stop, 1);
VHD_UnlockSlotHandle(slot);
break;
}
}
VHD_UnlockSlotHandle(slot);
} else if (r != VHDERR_TIMEOUT) {
fprintf(stderr, "[video:%u] VHD_LockSlotHandle error %lu — stopping\n",
ps->port, r);
atomic_store(&g_stop, 1);
break;
}
}
close(fd);
return NULL;
}
/* ── Parse comma-separated port list ─────────────────────────────────── */
static int parse_ports(const char *csv, unsigned *ports, int max) {
int count = 0;
char buf[256];
strncpy(buf, csv, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
char *tok = strtok(buf, ",");
while (tok && count < max) {
ports[count++] = (unsigned)atoi(tok);
tok = strtok(NULL, ",");
}
return count;
}
/* ── Main ─────────────────────────────────────────────────────────────── */
int main(int argc, char *argv[]) {
unsigned device_id = 0;
unsigned ports[MAX_PORTS] = {0};
int port_count = 0;
int sig_timeout = 30;
const char *video_pipe_dir = "/dev/shm/deltacast";
const char *audio_pipe_dir = "/dev/shm/deltacast";
for (int i = 1; i < argc; i++) {
if (!strcmp(argv[i], "--device") && i+1 < argc) {
device_id = (unsigned)atoi(argv[++i]);
} else if (!strcmp(argv[i], "--ports") && i+1 < argc) {
port_count = parse_ports(argv[++i], ports, MAX_PORTS);
} else if (!strcmp(argv[i], "--port") && i+1 < argc) {
/* single-port compat alias */
ports[0] = (unsigned)atoi(argv[++i]);
port_count = 1;
} else if (!strcmp(argv[i], "--video-pipe-dir") && i+1 < argc) {
video_pipe_dir = argv[++i];
} else if (!strcmp(argv[i], "--audio-pipe-dir") && i+1 < argc) {
audio_pipe_dir = argv[++i];
} else if (!strcmp(argv[i], "--signal-timeout") && i+1 < argc) {
sig_timeout = atoi(argv[++i]);
}
}
if (port_count == 0) {
fprintf(stderr, "{\"error\":\"no ports specified — use --ports 0,1,2,...\"}\n");
return 1;
}
signal(SIGINT, on_signal);
signal(SIGTERM, on_signal);
signal(SIGPIPE, SIG_IGN);
/* ── Init API ────────────────────────────────────────────────────── */
ULONG dll_ver, nb_boards;
if (VHD_GetApiInfo(&dll_ver, &nb_boards) != VHDERR_NOERROR) {
fprintf(stderr, "{\"error\":\"VHD_GetApiInfo failed\"}\n");
return 1;
}
if (device_id >= nb_boards) {
fprintf(stderr, "{\"error\":\"board %u not found (%lu detected)\"}\n",
device_id, nb_boards);
return 1;
}
/* ── Open board ONCE ─────────────────────────────────────────────── */
HANDLE board = NULL;
if (VHD_OpenBoardHandle(device_id, &board, NULL, 0) != VHDERR_NOERROR) {
fprintf(stderr, "{\"error\":\"VHD_OpenBoardHandle failed for board %u\"}\n", device_id);
return 1;
}
fprintf(stderr, "[board] opened board %u with %d port(s)\n", device_id, port_count);
/* Disable passive loopback for each requested port (ports 0-3 only in SDK). */
for (int pi = 0; pi < port_count; pi++) {
unsigned p = ports[pi];
if (p < 4) VHD_SetBoardProperty(board, loopback_prop(p), FALSE);
}
/* ── Wait for signal on all ports ───────────────────────────────── */
ULONG video_stds[MAX_PORTS] = {0};
ULONG clock_divs[MAX_PORTS] = {0};
int locked[MAX_PORTS] = {0};
for (int pi = 0; pi < port_count; pi++) {
video_stds[pi] = (ULONG)NB_VHD_VIDEOSTANDARDS;
clock_divs[pi] = VHD_CLOCKDIV_1;
}
struct timespec deadline;
clock_gettime(CLOCK_MONOTONIC, &deadline);
deadline.tv_sec += sig_timeout;
while (!atomic_load(&g_stop)) {
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
if (now.tv_sec > deadline.tv_sec ||
(now.tv_sec == deadline.tv_sec && now.tv_nsec >= deadline.tv_nsec)) break;
int all_locked = 1;
for (int pi = 0; pi < port_count; pi++) {
if (locked[pi]) continue;
VHD_GetChannelProperty(board, VHD_RX_CHANNEL, ports[pi],
VHD_SDI_CP_VIDEO_STANDARD, &video_stds[pi]);
if (video_stds[pi] != (ULONG)NB_VHD_VIDEOSTANDARDS) {
VHD_GetChannelProperty(board, VHD_RX_CHANNEL, ports[pi],
VHD_SDI_CP_CLOCK_DIVISOR, &clock_divs[pi]);
locked[pi] = 1;
fprintf(stderr, "[board] port %u signal locked (std=%lu)\n",
ports[pi], video_stds[pi]);
} else {
all_locked = 0;
}
}
if (all_locked) break;
struct timespec ts = {0, 200000000L}; /* 200ms poll */
nanosleep(&ts, NULL);
}
/* Report results — continue with whatever locked, abort only if NONE locked. */
int any_locked = 0;
for (int pi = 0; pi < port_count; pi++) {
if (locked[pi]) { any_locked = 1; }
else {
fprintf(stderr,
"{\"error\":\"no signal on board %u port %u within %ds\"}\n",
device_id, ports[pi], sig_timeout);
}
}
if (!any_locked || atomic_load(&g_stop)) {
VHD_CloseBoardHandle(board);
return 1;
}
/* ── Create FIFOs and open streams for each locked port ─────────── */
PortState ps[MAX_PORTS];
memset(ps, 0, sizeof(ps));
int active_count = 0;
for (int pi = 0; pi < port_count; pi++) {
if (!locked[pi]) continue;
PortState *p = &ps[active_count];
p->board = board;
p->port = ports[pi];
p->device = device_id;
p->video_std = video_stds[pi];
p->clock_div = clock_divs[pi];
p->vi = video_info((VHD_VIDEOSTANDARD)video_stds[pi],
(VHD_CLOCKDIVISOR)clock_divs[pi]);
snprintf(p->video_fifo, sizeof(p->video_fifo),
"%s/video-%u.fifo", video_pipe_dir, ports[pi]);
snprintf(p->audio_fifo, sizeof(p->audio_fifo),
"%s/audio-%u.fifo", audio_pipe_dir, ports[pi]);
/* Create FIFOs (mkfifo; ignore EEXIST). */
if (mkfifo(p->video_fifo, 0666) != 0 && errno != EEXIST) {
fprintf(stderr, "[port:%u] mkfifo video failed: %s\n", ports[pi], strerror(errno));
continue;
}
if (mkfifo(p->audio_fifo, 0666) != 0 && errno != EEXIST) {
fprintf(stderr, "[port:%u] mkfifo audio failed: %s\n", ports[pi], strerror(errno));
continue;
}
/* Open video stream. */
HANDLE vs = NULL;
ULONG r = VHD_OpenStreamHandle(board, rx_streamtype(ports[pi]),
VHD_SDI_STPROC_DISJOINED_VIDEO,
NULL, &vs, NULL);
if (r != VHDERR_NOERROR) {
fprintf(stderr, "{\"error\":\"VHD_OpenStreamHandle video failed port %u rc=%lu\"}\n",
ports[pi], r);
continue;
}
VHD_SetStreamProperty(vs, VHD_SDI_SP_VIDEO_STANDARD, p->video_std);
VHD_SetStreamProperty(vs, VHD_SDI_SP_CLOCK_SYSTEM, p->clock_div);
VHD_SetStreamProperty(vs, VHD_CORE_SP_TRANSFER_SCHEME, VHD_TRANSFER_SLAVED);
VHD_SetStreamProperty(vs, VHD_CORE_SP_BUFFERQUEUE_DEPTH, 8);
p->video_stream = vs;
if (VHD_StartStream(vs) != VHDERR_NOERROR) {
fprintf(stderr, "{\"error\":\"VHD_StartStream video failed port %u\"}\n", ports[pi]);
VHD_CloseStreamHandle(vs);
p->video_stream = NULL;
continue;
}
/* Emit format JSON to stderr (one line per port on signal lock). */
fprintf(stderr,
"{\"port\":%u,\"width\":%d,\"height\":%d,"
"\"fps_num\":%d,\"fps_den\":%d,"
"\"interlaced\":%s,"
"\"pix_fmt\":\"uyvy422\","
"\"audio_channels\":2,\"audio_rate\":48000,"
"\"device\":%u}\n",
ports[pi],
p->vi.width, p->vi.height,
p->vi.fps_num, p->vi.fps_den,
p->vi.interlaced ? "true" : "false",
device_id);
fflush(stderr);
/* Launch audio thread (blocks until reader connects to audio FIFO). */
pthread_create(&p->audio_tid, NULL, audio_thread, p);
/* Launch video thread (blocks until reader connects to video FIFO). */
pthread_create(&p->video_tid, NULL, video_thread, p);
active_count++;
}
if (active_count == 0) {
fprintf(stderr, "{\"error\":\"no ports successfully started\"}\n");
VHD_CloseBoardHandle(board);
return 1;
}
/* ── Wait for all threads to finish ─────────────────────────────── */
for (int i = 0; i < active_count; i++) {
if (ps[i].video_tid) pthread_join(ps[i].video_tid, NULL);
if (ps[i].audio_tid) pthread_join(ps[i].audio_tid, NULL);
}
/* ── Cleanup ─────────────────────────────────────────────────────── */
for (int i = 0; i < active_count; i++) {
if (ps[i].video_stream) {
VHD_StopStream(ps[i].video_stream);
VHD_CloseStreamHandle(ps[i].video_stream);
}
}
VHD_CloseBoardHandle(board);
return 0;
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,338 @@
/* -LICENSE-START-
** Copyright (c) 2026 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation covered by
** this license (the "Software") to use, reproduce, display, distribute,
** execute, and transmit the Software, and to prepare derivative works of the
** Software, and to permit third-parties to whom the Software is furnished to
** do so, all subject to the following:
**
** The copyright notices in the Software and this entire statement, including
** the above license grant, this restriction and the following disclaimer,
** must be included in all copies of the Software, in whole or in part, and
** all derivative works of the Software, unless such copies or derivative
** works are solely in the form of machine-executable object code generated by
** a source language processor.
**
** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
** -LICENSE-END-
*/
/*
* -- AUTOMATICALLY GENERATED - DO NOT EDIT ---
*/
#ifndef BMD_DECKLINKAPICONFIGURATION_H
#define BMD_DECKLINKAPICONFIGURATION_H
#ifndef BMD_CONST
#if defined(_MSC_VER)
#define BMD_CONST __declspec(selectany) static const
#else
#define BMD_CONST static const
#endif
#endif
#ifndef BMD_PUBLIC
#define BMD_PUBLIC
#endif
// Type Declarations
// Interface ID Declarations
BMD_CONST REFIID IID_IDeckLinkConfiguration = /* 5A68FFD4-1C12-4EDE-A6D2-45451D385FC1 */ { 0x5A,0x68,0xFF,0xD4,0x1C,0x12,0x4E,0xDE,0xA6,0xD2,0x45,0x45,0x1D,0x38,0x5F,0xC1 };
BMD_CONST REFIID IID_IDeckLinkEncoderConfiguration = /* 138050E5-C60A-4552-BF3F-0F358049327E */ { 0x13,0x80,0x50,0xE5,0xC6,0x0A,0x45,0x52,0xBF,0x3F,0x0F,0x35,0x80,0x49,0x32,0x7E };
/* Enum BMDDeckLinkConfigurationID - DeckLink Configuration ID */
typedef uint32_t BMDDeckLinkConfigurationID;
enum _BMDDeckLinkConfigurationID {
/* Serial port Flags */
bmdDeckLinkConfigSwapSerialRxTx = /* 'ssrt' */ 0x73737274,
/* Video Input/Output Integers */
bmdDeckLinkConfigHDMI3DPackingFormat = /* '3dpf' */ 0x33647066,
bmdDeckLinkConfigBypass = /* 'byps' */ 0x62797073,
bmdDeckLinkConfigClockTimingAdjustment = /* 'ctad' */ 0x63746164,
bmdDeckLinkConfigAudioMeterType = /* 'aumt' */ 0x61756D74,
/* Audio Input/Output Flags */
bmdDeckLinkConfigAnalogAudioConsumerLevels = /* 'aacl' */ 0x6161636C,
bmdDeckLinkConfigSwapHDMICh3AndCh4OnInput = /* 'hi34' */ 0x68693334,
bmdDeckLinkConfigSwapHDMICh3AndCh4OnOutput = /* 'ho34' */ 0x686F3334,
bmdDeckLinkConfigAnalogAudioOutputChannelsMutedByHeadphone = /* 'amhp' */ 0x616D6870,
bmdDeckLinkConfigAnalogAudioOutputChannelsMutedBySpeaker = /* 'amsp' */ 0x616D7370,
/* Video Output Flags */
bmdDeckLinkConfigFieldFlickerRemoval = /* 'fdfr' */ 0x66646672,
bmdDeckLinkConfigHD1080p24ToHD1080i5994Conversion = /* 'to59' */ 0x746F3539,
bmdDeckLinkConfig444SDIVideoOutput = /* '444o' */ 0x3434346F,
bmdDeckLinkConfigBlackVideoOutputDuringCapture = /* 'bvoc' */ 0x62766F63,
bmdDeckLinkConfigLowLatencyVideoOutput = /* 'llvo' */ 0x6C6C766F,
bmdDeckLinkConfigDownConversionOnAllAnalogOutput = /* 'caao' */ 0x6361616F,
bmdDeckLinkConfigSMPTELevelAOutput = /* 'smta' */ 0x736D7461,
bmdDeckLinkConfigRec2020Output = /* 'rec2' */ 0x72656332, // Ensure output is Rec.2020 colorspace
bmdDeckLinkConfigQuadLinkSDIVideoOutputSquareDivisionSplit = /* 'SDQS' */ 0x53445153,
bmdDeckLinkConfigOutput1080pAsPsF = /* 'pfpr' */ 0x70667072,
bmdDeckLinkConfigOutputValidateEDIDForDolbyVision = /* 'pred' */ 0x70726564,
bmdDeckLinkConfigExtendedDesktop = /* 'exdt' */ 0x65786474,
bmdDeckLinkConfigEthernetVideoOutputIP10 = /* 'IP10' */ 0x49503130,
/* Video Output Integers */
bmdDeckLinkConfigVideoOutputConnection = /* 'vocn' */ 0x766F636E,
bmdDeckLinkConfigVideoOutputConversionMode = /* 'vocm' */ 0x766F636D,
bmdDeckLinkConfigVideoOutputConversionColorspaceDestination = /* 'vccd' */ 0x76636364, // Parameter is of type BMDColorspace
bmdDeckLinkConfigVideoOutputConversionColorspaceSource = /* 'vccs' */ 0x76636373, // Parameter is of type BMDColorspace
bmdDeckLinkConfigAnalogVideoOutputFlags = /* 'avof' */ 0x61766F66,
bmdDeckLinkConfigReferenceInputTimingOffset = /* 'glot' */ 0x676C6F74,
bmdDeckLinkConfigReferenceOutputMode = /* 'glOm' */ 0x676C4F6D,
bmdDeckLinkConfigVideoOutputIdleOperation = /* 'voio' */ 0x766F696F,
bmdDeckLinkConfigDefaultVideoOutputMode = /* 'dvom' */ 0x64766F6D,
bmdDeckLinkConfigDefaultVideoOutputModeFlags = /* 'dvof' */ 0x64766F66,
bmdDeckLinkConfigSDIOutputLinkConfiguration = /* 'solc' */ 0x736F6C63,
bmdDeckLinkConfigHDMITimecodePacking = /* 'htpk' */ 0x6874706B,
bmdDeckLinkConfigPlaybackGroup = /* 'plgr' */ 0x706C6772,
/* Video Output Floats */
bmdDeckLinkConfigVideoOutputComponentLumaGain = /* 'oclg' */ 0x6F636C67,
bmdDeckLinkConfigVideoOutputComponentChromaBlueGain = /* 'occb' */ 0x6F636362,
bmdDeckLinkConfigVideoOutputComponentChromaRedGain = /* 'occr' */ 0x6F636372,
bmdDeckLinkConfigVideoOutputCompositeLumaGain = /* 'oilg' */ 0x6F696C67,
bmdDeckLinkConfigVideoOutputCompositeChromaGain = /* 'oicg' */ 0x6F696367,
bmdDeckLinkConfigVideoOutputSVideoLumaGain = /* 'oslg' */ 0x6F736C67,
bmdDeckLinkConfigVideoOutputSVideoChromaGain = /* 'oscg' */ 0x6F736367,
bmdDeckLinkConfigDolbyVisionCMVersion = /* 'dvvr' */ 0x64767672,
bmdDeckLinkConfigDolbyVisionMasterMinimumNits = /* 'mnnt' */ 0x6D6E6E74,
bmdDeckLinkConfigDolbyVisionMasterMaximumNits = /* 'mxnt' */ 0x6D786E74,
/* Video Input Flags */
bmdDeckLinkConfigVideoInputScanning = /* 'visc' */ 0x76697363, // Applicable to H264 Pro Recorder only
bmdDeckLinkConfigUseDedicatedLTCInput = /* 'dltc' */ 0x646C7463, // Use timecode from LTC input instead of SDI stream
bmdDeckLinkConfigSDIInput3DPayloadOverride = /* '3dds' */ 0x33646473,
bmdDeckLinkConfigCapture1080pAsPsF = /* 'cfpr' */ 0x63667072,
/* Video Input Integers */
bmdDeckLinkConfigVideoInputConnection = /* 'vicn' */ 0x7669636E,
bmdDeckLinkConfigAnalogVideoInputFlags = /* 'avif' */ 0x61766966,
bmdDeckLinkConfigVideoInputConversionMode = /* 'vicm' */ 0x7669636D,
bmdDeckLinkConfig32PulldownSequenceInitialTimecodeFrame = /* 'pdif' */ 0x70646966,
bmdDeckLinkConfigVANCSourceLine1Mapping = /* 'vsl1' */ 0x76736C31,
bmdDeckLinkConfigVANCSourceLine2Mapping = /* 'vsl2' */ 0x76736C32,
bmdDeckLinkConfigVANCSourceLine3Mapping = /* 'vsl3' */ 0x76736C33,
bmdDeckLinkConfigCapturePassThroughMode = /* 'cptm' */ 0x6370746D,
bmdDeckLinkConfigCaptureGroup = /* 'cpgr' */ 0x63706772,
bmdDeckLinkConfigHANCInputFilter1 = /* 'hif1' */ 0x68696631,
bmdDeckLinkConfigHANCInputFilter2 = /* 'hif2' */ 0x68696632,
bmdDeckLinkConfigHANCInputFilter3 = /* 'hif3' */ 0x68696633,
bmdDeckLinkConfigHANCInputFilter4 = /* 'hif4' */ 0x68696634,
/* Video Input Floats */
bmdDeckLinkConfigVideoInputComponentLumaGain = /* 'iclg' */ 0x69636C67,
bmdDeckLinkConfigVideoInputComponentChromaBlueGain = /* 'iccb' */ 0x69636362,
bmdDeckLinkConfigVideoInputComponentChromaRedGain = /* 'iccr' */ 0x69636372,
bmdDeckLinkConfigVideoInputCompositeLumaGain = /* 'iilg' */ 0x69696C67,
bmdDeckLinkConfigVideoInputCompositeChromaGain = /* 'iicg' */ 0x69696367,
bmdDeckLinkConfigVideoInputSVideoLumaGain = /* 'islg' */ 0x69736C67,
bmdDeckLinkConfigVideoInputSVideoChromaGain = /* 'iscg' */ 0x69736367,
/* Keying Integers */
bmdDeckLinkConfigInternalKeyingAncillaryDataSource = /* 'ikas' */ 0x696B6173,
/* Audio Input Flags */
bmdDeckLinkConfigMicrophonePhantomPower = /* 'mphp' */ 0x6D706870,
/* Audio Input Integers */
bmdDeckLinkConfigAudioInputConnection = /* 'aicn' */ 0x6169636E,
/* Audio Input Floats */
bmdDeckLinkConfigAnalogAudioInputScaleChannel1 = /* 'ais1' */ 0x61697331,
bmdDeckLinkConfigAnalogAudioInputScaleChannel2 = /* 'ais2' */ 0x61697332,
bmdDeckLinkConfigAnalogAudioInputScaleChannel3 = /* 'ais3' */ 0x61697333,
bmdDeckLinkConfigAnalogAudioInputScaleChannel4 = /* 'ais4' */ 0x61697334,
bmdDeckLinkConfigDigitalAudioInputScale = /* 'dais' */ 0x64616973,
bmdDeckLinkConfigMicrophoneInputGain = /* 'micg' */ 0x6D696367,
bmdDeckLinkConfigAudioOutputXLRDelayFrames = /* 'xdfr' */ 0x78646672,
/* Audio Output Integers */
bmdDeckLinkConfigAudioOutputAESAnalogSwitch = /* 'aoaa' */ 0x616F6161,
bmdDeckLinkConfigAudioOutputXLRDelayTime = /* 'xdms' */ 0x78646D73,
bmdDeckLinkConfigAudioOutputXLRDelayType = /* 'xdty' */ 0x78647479,
/* Audio Output Floats */
bmdDeckLinkConfigAnalogAudioOutputScaleChannel1 = /* 'aos1' */ 0x616F7331,
bmdDeckLinkConfigAnalogAudioOutputScaleChannel2 = /* 'aos2' */ 0x616F7332,
bmdDeckLinkConfigAnalogAudioOutputScaleChannel3 = /* 'aos3' */ 0x616F7333,
bmdDeckLinkConfigAnalogAudioOutputScaleChannel4 = /* 'aos4' */ 0x616F7334,
bmdDeckLinkConfigDigitalAudioOutputScale = /* 'daos' */ 0x64616F73,
bmdDeckLinkConfigHeadphoneVolume = /* 'hvol' */ 0x68766F6C,
bmdDeckLinkConfigSpeakerVolume = /* 'svol' */ 0x73766F6C,
/* Ethernet Flags */
bmdDeckLinkConfigEthernetPTPFollowerOnly = /* 'PTPf' */ 0x50545066,
bmdDeckLinkConfigEthernetPTPUseUDPEncapsulation = /* 'PTPU' */ 0x50545055,
bmdDeckLinkConfigEthernetUseManualNMOSRegistry = /* 'nmrp' */ 0x6E6D7270,
/* Ethernet Integers */
bmdDeckLinkConfigEthernetPTPPriority1 = /* 'PTP1' */ 0x50545031,
bmdDeckLinkConfigEthernetPTPPriority2 = /* 'PTP2' */ 0x50545032,
bmdDeckLinkConfigEthernetPTPDomain = /* 'PTPD' */ 0x50545044,
bmdDeckLinkConfigEthernetPTPLogAnnounceInterval = /* 'PTPA' */ 0x50545041,
/* Ethernet Strings */
bmdDeckLinkConfigEthernetAudioOutputChannelOrder = /* 'caco' */ 0x6361636F,
bmdDeckLinkConfigEthernetNMOSRegistryAddress = /* 'nmre' */ 0x6E6D7265,
/* Parameterized Ethernet Flags */
bmdDeckLinkConfigParamEthernetUseDHCP = /* 'DHCP' */ 0x44484350,
/* Parameterized Ethernet Strings */
bmdDeckLinkConfigParamEthernetStaticLocalIPAddress = /* 'nsip' */ 0x6E736970,
bmdDeckLinkConfigParamEthernetStaticSubnetMask = /* 'nssm' */ 0x6E73736D,
bmdDeckLinkConfigParamEthernetStaticGatewayIPAddress = /* 'nsgw' */ 0x6E736777,
bmdDeckLinkConfigParamEthernetStaticPrimaryDNS = /* 'nspd' */ 0x6E737064,
bmdDeckLinkConfigParamEthernetStaticSecondaryDNS = /* 'nssd' */ 0x6E737364,
bmdDeckLinkConfigParamEthernetVideoOutputAddress = /* 'noav' */ 0x6E6F6176,
bmdDeckLinkConfigParamEthernetAudioOutputAddress = /* 'noaa' */ 0x6E6F6161,
bmdDeckLinkConfigParamEthernetAncillaryOutputAddress = /* 'noaA' */ 0x6E6F6141,
/* Device Information Strings */
bmdDeckLinkConfigDeviceInformationLabel = /* 'dila' */ 0x64696C61,
bmdDeckLinkConfigDeviceInformationSerialNumber = /* 'disn' */ 0x6469736E,
bmdDeckLinkConfigDeviceInformationCompany = /* 'dico' */ 0x6469636F,
bmdDeckLinkConfigDeviceInformationPhone = /* 'diph' */ 0x64697068,
bmdDeckLinkConfigDeviceInformationEmail = /* 'diem' */ 0x6469656D,
bmdDeckLinkConfigDeviceInformationDate = /* 'dida' */ 0x64696461,
/* Deck Control Integers */
bmdDeckLinkConfigDeckControlConnection = /* 'dcco' */ 0x6463636F,
/* UI/UX Integers */
bmdDeckLinkConfigDisplayLanguage = /* 'lang' */ 0x6C616E67
};
/* Enum BMDDeckLinkEncoderConfigurationID - DeckLink Encoder Configuration ID */
typedef uint32_t BMDDeckLinkEncoderConfigurationID;
enum _BMDDeckLinkEncoderConfigurationID {
/* Video Encoder Integers */
bmdDeckLinkEncoderConfigPreferredBitDepth = /* 'epbr' */ 0x65706272,
bmdDeckLinkEncoderConfigFrameCodingMode = /* 'efcm' */ 0x6566636D,
/* HEVC/H.265 Encoder Integers */
bmdDeckLinkEncoderConfigH265TargetBitrate = /* 'htbr' */ 0x68746272,
/* DNxHR/DNxHD Compression ID */
bmdDeckLinkEncoderConfigDNxHRCompressionID = /* 'dcid' */ 0x64636964,
/* DNxHR/DNxHD Level */
bmdDeckLinkEncoderConfigDNxHRLevel = /* 'dlev' */ 0x646C6576,
/* Encoded Sample Decriptions */
bmdDeckLinkEncoderConfigMPEG4SampleDescription = /* 'stsE' */ 0x73747345, // Full MPEG4 sample description (aka SampleEntry of an 'stsd' atom-box). Useful for MediaFoundation, QuickTime, MKV and more
bmdDeckLinkEncoderConfigMPEG4CodecSpecificDesc = /* 'esds' */ 0x65736473 // Sample description extensions only (atom stream, each with size and fourCC header). Useful for AVFoundation, VideoToolbox, MKV and more
};
#if defined(__cplusplus)
// Forward Declarations
class IDeckLinkConfiguration;
class IDeckLinkEncoderConfiguration;
/* Interface IDeckLinkConfiguration - DeckLink Configuration interface */
class BMD_PUBLIC IDeckLinkConfiguration : public IUnknown
{
public:
virtual HRESULT SetFlag (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ bool value) = 0;
virtual HRESULT GetFlag (/* in */ BMDDeckLinkConfigurationID cfgID, /* out */ bool* value) = 0;
virtual HRESULT SetInt (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ int64_t value) = 0;
virtual HRESULT GetInt (/* in */ BMDDeckLinkConfigurationID cfgID, /* out */ int64_t* value) = 0;
virtual HRESULT SetFloat (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ double value) = 0;
virtual HRESULT GetFloat (/* in */ BMDDeckLinkConfigurationID cfgID, /* out */ double* value) = 0;
virtual HRESULT SetString (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ const char* value) = 0;
virtual HRESULT GetString (/* in */ BMDDeckLinkConfigurationID cfgID, /* out */ const char** value) = 0;
virtual HRESULT SetFlagWithParam (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ uint64_t param, /* in */ bool value) = 0;
virtual HRESULT GetFlagWithParam (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ uint64_t param, /* out */ bool* value) = 0;
virtual HRESULT SetIntWithParam (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ uint64_t param, /* in */ int64_t value) = 0;
virtual HRESULT GetIntWithParam (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ uint64_t param, /* out */ int64_t* value) = 0;
virtual HRESULT SetFloatWithParam (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ uint64_t param, /* in */ double value) = 0;
virtual HRESULT GetFloatWithParam (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ uint64_t param, /* out */ double* value) = 0;
virtual HRESULT SetStringWithParam (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ uint64_t param, /* in */ const char* value) = 0;
virtual HRESULT GetStringWithParam (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ uint64_t param, /* out */ const char** value) = 0;
virtual HRESULT WriteConfigurationToPreferences (void) = 0;
protected:
virtual ~IDeckLinkConfiguration () {} // call Release method to drop reference count
};
/* Interface IDeckLinkEncoderConfiguration - DeckLink Encoder Configuration interface. Obtained from IDeckLinkEncoderInput */
class BMD_PUBLIC IDeckLinkEncoderConfiguration : public IUnknown
{
public:
virtual HRESULT SetFlag (/* in */ BMDDeckLinkEncoderConfigurationID cfgID, /* in */ bool value) = 0;
virtual HRESULT GetFlag (/* in */ BMDDeckLinkEncoderConfigurationID cfgID, /* out */ bool* value) = 0;
virtual HRESULT SetInt (/* in */ BMDDeckLinkEncoderConfigurationID cfgID, /* in */ int64_t value) = 0;
virtual HRESULT GetInt (/* in */ BMDDeckLinkEncoderConfigurationID cfgID, /* out */ int64_t* value) = 0;
virtual HRESULT SetFloat (/* in */ BMDDeckLinkEncoderConfigurationID cfgID, /* in */ double value) = 0;
virtual HRESULT GetFloat (/* in */ BMDDeckLinkEncoderConfigurationID cfgID, /* out */ double* value) = 0;
virtual HRESULT SetString (/* in */ BMDDeckLinkEncoderConfigurationID cfgID, /* in */ const char* value) = 0;
virtual HRESULT GetString (/* in */ BMDDeckLinkEncoderConfigurationID cfgID, /* out */ const char** value) = 0;
virtual HRESULT GetBytes (/* in */ BMDDeckLinkEncoderConfigurationID cfgID, /* out */ void* buffer /* optional */, /* in, out */ uint32_t* bufferSize) = 0;
protected:
virtual ~IDeckLinkEncoderConfiguration () {} // call Release method to drop reference count
};
/* Functions */
extern "C" {
}
#endif /* defined(__cplusplus) */
#endif /* defined(BMD_DECKLINKAPICONFIGURATION_H) */

View file

@ -0,0 +1,84 @@
/* -LICENSE-START-
** Copyright (c) 2017 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation (the
** "Software") to use, reproduce, display, distribute, sub-license, execute,
** and transmit the Software, and to prepare derivative works of the Software,
** and to permit third-parties to whom the Software is furnished to do so, in
** accordance with:
**
** (1) if the Software is obtained from Blackmagic Design, the End User License
** Agreement for the Software Development Kit ("EULA") available at
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
**
** (2) if the Software is obtained from any third party, such licensing terms
** as notified by that third party,
**
** and all subject to the following:
**
** (3) the copyright notices in the Software and this entire statement,
** including the above license grant, this restriction and the following
** disclaimer, must be included in all copies of the Software, in whole or in
** part, and all derivative works of the Software, unless such copies or
** derivative works are solely in the form of machine-executable object code
** generated by a source language processor.
**
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
**
** A copy of the Software is available free of charge at
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
**
** -LICENSE-END-
*/
#ifndef BMD_DECKLINKAPICONFIGURATION_v10_11_H
#define BMD_DECKLINKAPICONFIGURATION_v10_11_H
#include "DeckLinkAPIConfiguration.h"
// Interface ID Declarations
BMD_CONST REFIID IID_IDeckLinkConfiguration_v10_11 = /* EF90380B-4AE5-4346-9077-E288E149F129 */ {0xEF,0x90,0x38,0x0B,0x4A,0xE5,0x43,0x46,0x90,0x77,0xE2,0x88,0xE1,0x49,0xF1,0x29};
/* Enum BMDDeckLinkConfigurationID_v10_11 - DeckLink Configuration ID */
typedef uint32_t BMDDeckLinkConfigurationID_v10_11;
enum _BMDDeckLinkConfigurationID_v10_11 {
/* Video Input/Output Integers */
bmdDeckLinkConfigDuplexMode_v10_11 = /* 'dupx' */ 0x64757078,
};
// Forward Declarations
class IDeckLinkConfiguration_v10_11;
/* Interface IDeckLinkConfiguration_v10_11 - DeckLink Configuration interface */
class BMD_PUBLIC IDeckLinkConfiguration_v10_11 : public IUnknown
{
public:
virtual HRESULT SetFlag (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ bool value) = 0;
virtual HRESULT GetFlag (/* in */ BMDDeckLinkConfigurationID cfgID, /* out */ bool *value) = 0;
virtual HRESULT SetInt (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ int64_t value) = 0;
virtual HRESULT GetInt (/* in */ BMDDeckLinkConfigurationID cfgID, /* out */ int64_t *value) = 0;
virtual HRESULT SetFloat (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ double value) = 0;
virtual HRESULT GetFloat (/* in */ BMDDeckLinkConfigurationID cfgID, /* out */ double *value) = 0;
virtual HRESULT SetString (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ const char *value) = 0;
virtual HRESULT GetString (/* in */ BMDDeckLinkConfigurationID cfgID, /* out */ const char **value) = 0;
virtual HRESULT WriteConfigurationToPreferences (void) = 0;
protected:
virtual ~IDeckLinkConfiguration_v10_11 () {} // call Release method to drop reference count
};
#endif /* defined(BMD_DECKLINKAPICONFIGURATION_v10_11_H) */

View file

@ -0,0 +1,73 @@
/* -LICENSE-START-
** Copyright (c) 2014 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation (the
** "Software") to use, reproduce, display, distribute, sub-license, execute,
** and transmit the Software, and to prepare derivative works of the Software,
** and to permit third-parties to whom the Software is furnished to do so, in
** accordance with:
**
** (1) if the Software is obtained from Blackmagic Design, the End User License
** Agreement for the Software Development Kit ("EULA") available at
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
**
** (2) if the Software is obtained from any third party, such licensing terms
** as notified by that third party,
**
** and all subject to the following:
**
** (3) the copyright notices in the Software and this entire statement,
** including the above license grant, this restriction and the following
** disclaimer, must be included in all copies of the Software, in whole or in
** part, and all derivative works of the Software, unless such copies or
** derivative works are solely in the form of machine-executable object code
** generated by a source language processor.
**
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
**
** A copy of the Software is available free of charge at
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
**
** -LICENSE-END-
*/
#ifndef BMD_DECKLINKAPICONFIGURATION_v10_2_H
#define BMD_DECKLINKAPICONFIGURATION_v10_2_H
#include "DeckLinkAPIConfiguration.h"
// Interface ID Declarations
BMD_CONST REFIID IID_IDeckLinkConfiguration_v10_2 = /* C679A35B-610C-4D09-B748-1D0478100FC0 */ {0xC6,0x79,0xA3,0x5B,0x61,0x0C,0x4D,0x09,0xB7,0x48,0x1D,0x04,0x78,0x10,0x0F,0xC0};
// Forward Declarations
class IDeckLinkConfiguration_v10_2;
/* Interface IDeckLinkConfiguration_v10_2 - DeckLink Configuration interface */
class BMD_PUBLIC IDeckLinkConfiguration_v10_2 : public IUnknown
{
public:
virtual HRESULT SetFlag (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ bool value) = 0;
virtual HRESULT GetFlag (/* in */ BMDDeckLinkConfigurationID cfgID, /* out */ bool *value) = 0;
virtual HRESULT SetInt (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ int64_t value) = 0;
virtual HRESULT GetInt (/* in */ BMDDeckLinkConfigurationID cfgID, /* out */ int64_t *value) = 0;
virtual HRESULT SetFloat (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ double value) = 0;
virtual HRESULT GetFloat (/* in */ BMDDeckLinkConfigurationID cfgID, /* out */ double *value) = 0;
virtual HRESULT SetString (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ const char *value) = 0;
virtual HRESULT GetString (/* in */ BMDDeckLinkConfigurationID cfgID, /* out */ const char **value) = 0;
virtual HRESULT WriteConfigurationToPreferences (void) = 0;
protected:
virtual ~IDeckLinkConfiguration_v10_2 () {} // call Release method to drop reference count
};
#endif /* defined(BMD_DECKLINKAPICONFIGURATION_v10_2_H) */

View file

@ -0,0 +1,76 @@
/* -LICENSE-START-
** Copyright (c) 2015 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation (the
** "Software") to use, reproduce, display, distribute, sub-license, execute,
** and transmit the Software, and to prepare derivative works of the Software,
** and to permit third-parties to whom the Software is furnished to do so, in
** accordance with:
**
** (1) if the Software is obtained from Blackmagic Design, the End User License
** Agreement for the Software Development Kit ("EULA") available at
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
**
** (2) if the Software is obtained from any third party, such licensing terms
** as notified by that third party,
**
** and all subject to the following:
**
** (3) the copyright notices in the Software and this entire statement,
** including the above license grant, this restriction and the following
** disclaimer, must be included in all copies of the Software, in whole or in
** part, and all derivative works of the Software, unless such copies or
** derivative works are solely in the form of machine-executable object code
** generated by a source language processor.
**
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
**
** A copy of the Software is available free of charge at
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
**
** -LICENSE-END-
*/
#ifndef BMD_DECKLINKAPICONFIGURATION_v10_4_H
#define BMD_DECKLINKAPICONFIGURATION_v10_4_H
#include "DeckLinkAPIConfiguration.h"
// Interface ID Declarations
BMD_CONST REFIID IID_IDeckLinkConfiguration_v10_4 = /* 1E69FCF6-4203-4936-8076-2A9F4CFD50CB */ {0x1E,0x69,0xFC,0xF6,0x42,0x03,0x49,0x36,0x80,0x76,0x2A,0x9F,0x4C,0xFD,0x50,0xCB};
//
// Forward Declarations
class IDeckLinkConfiguration_v10_4;
/* Interface IDeckLinkConfiguration_v10_4 - DeckLink Configuration interface */
class BMD_PUBLIC IDeckLinkConfiguration_v10_4 : public IUnknown
{
public:
virtual HRESULT SetFlag (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ bool value) = 0;
virtual HRESULT GetFlag (/* in */ BMDDeckLinkConfigurationID cfgID, /* out */ bool *value) = 0;
virtual HRESULT SetInt (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ int64_t value) = 0;
virtual HRESULT GetInt (/* in */ BMDDeckLinkConfigurationID cfgID, /* out */ int64_t *value) = 0;
virtual HRESULT SetFloat (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ double value) = 0;
virtual HRESULT GetFloat (/* in */ BMDDeckLinkConfigurationID cfgID, /* out */ double *value) = 0;
virtual HRESULT SetString (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ const char *value) = 0;
virtual HRESULT GetString (/* in */ BMDDeckLinkConfigurationID cfgID, /* out */ const char **value) = 0;
virtual HRESULT WriteConfigurationToPreferences (void) = 0;
protected:
virtual ~IDeckLinkConfiguration_v10_4 () {} // call Release method to drop reference count
};
#endif /* defined(BMD_DECKLINKAPICONFIGURATION_v10_4_H) */

View file

@ -0,0 +1,73 @@
/* -LICENSE-START-
** Copyright (c) 2015 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation (the
** "Software") to use, reproduce, display, distribute, sub-license, execute,
** and transmit the Software, and to prepare derivative works of the Software,
** and to permit third-parties to whom the Software is furnished to do so, in
** accordance with:
**
** (1) if the Software is obtained from Blackmagic Design, the End User License
** Agreement for the Software Development Kit ("EULA") available at
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
**
** (2) if the Software is obtained from any third party, such licensing terms
** as notified by that third party,
**
** and all subject to the following:
**
** (3) the copyright notices in the Software and this entire statement,
** including the above license grant, this restriction and the following
** disclaimer, must be included in all copies of the Software, in whole or in
** part, and all derivative works of the Software, unless such copies or
** derivative works are solely in the form of machine-executable object code
** generated by a source language processor.
**
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
**
** A copy of the Software is available free of charge at
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
**
** -LICENSE-END-
*/
#ifndef BMD_DECKLINKAPICONFIGURATION_v10_5_H
#define BMD_DECKLINKAPICONFIGURATION_v10_5_H
#include "DeckLinkAPIConfiguration.h"
// Interface ID Declarations
BMD_CONST REFIID IID_IDeckLinkEncoderConfiguration_v10_5 = /* 67455668-0848-45DF-8D8E-350A77C9A028 */ {0x67,0x45,0x56,0x68,0x08,0x48,0x45,0xDF,0x8D,0x8E,0x35,0x0A,0x77,0xC9,0xA0,0x28};
// Forward Declarations
class IDeckLinkEncoderConfiguration_v10_5;
/* Interface IDeckLinkEncoderConfiguration_v10_5 - DeckLink Encoder Configuration interface. Obtained from IDeckLinkEncoderInput */
class BMD_PUBLIC IDeckLinkEncoderConfiguration_v10_5 : public IUnknown
{
public:
virtual HRESULT SetFlag (/* in */ BMDDeckLinkEncoderConfigurationID cfgID, /* in */ bool value) = 0;
virtual HRESULT GetFlag (/* in */ BMDDeckLinkEncoderConfigurationID cfgID, /* out */ bool *value) = 0;
virtual HRESULT SetInt (/* in */ BMDDeckLinkEncoderConfigurationID cfgID, /* in */ int64_t value) = 0;
virtual HRESULT GetInt (/* in */ BMDDeckLinkEncoderConfigurationID cfgID, /* out */ int64_t *value) = 0;
virtual HRESULT SetFloat (/* in */ BMDDeckLinkEncoderConfigurationID cfgID, /* in */ double value) = 0;
virtual HRESULT GetFloat (/* in */ BMDDeckLinkEncoderConfigurationID cfgID, /* out */ double *value) = 0;
virtual HRESULT SetString (/* in */ BMDDeckLinkEncoderConfigurationID cfgID, /* in */ const char *value) = 0;
virtual HRESULT GetString (/* in */ BMDDeckLinkEncoderConfigurationID cfgID, /* out */ const char **value) = 0;
virtual HRESULT GetDecoderConfigurationInfo (/* out */ void *buffer, /* in */ long bufferSize, /* out */ long *returnedSize) = 0;
protected:
virtual ~IDeckLinkEncoderConfiguration_v10_5 () {} // call Release method to drop reference count
};
#endif /* defined(BMD_DECKLINKAPICONFIGURATION_v10_5_H) */

View file

@ -0,0 +1,75 @@
/* -LICENSE-START-
** Copyright (c) 2017 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation (the
** "Software") to use, reproduce, display, distribute, sub-license, execute,
** and transmit the Software, and to prepare derivative works of the Software,
** and to permit third-parties to whom the Software is furnished to do so, in
** accordance with:
**
** (1) if the Software is obtained from Blackmagic Design, the End User License
** Agreement for the Software Development Kit ("EULA") available at
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
**
** (2) if the Software is obtained from any third party, such licensing terms
** as notified by that third party,
**
** and all subject to the following:
**
** (3) the copyright notices in the Software and this entire statement,
** including the above license grant, this restriction and the following
** disclaimer, must be included in all copies of the Software, in whole or in
** part, and all derivative works of the Software, unless such copies or
** derivative works are solely in the form of machine-executable object code
** generated by a source language processor.
**
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
**
** A copy of the Software is available free of charge at
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
**
** -LICENSE-END-
*/
#ifndef BMD_DECKLINKAPICONFIGURATION_v10_9_H
#define BMD_DECKLINKAPICONFIGURATION_v10_9_H
#include "DeckLinkAPIConfiguration.h"
// Interface ID Declarations
BMD_CONST REFIID IID_IDeckLinkConfiguration_v10_9 = /* CB71734A-FE37-4E8D-8E13-802133A1C3F2 */ {0xCB,0x71,0x73,0x4A,0xFE,0x37,0x4E,0x8D,0x8E,0x13,0x80,0x21,0x33,0xA1,0xC3,0xF2};
//
// Forward Declarations
class IDeckLinkConfiguration_v10_9;
/* Interface IDeckLinkConfiguration_v10_9 - DeckLink Configuration interface */
class BMD_PUBLIC IDeckLinkConfiguration_v10_9 : public IUnknown
{
public:
virtual HRESULT SetFlag (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ bool value) = 0;
virtual HRESULT GetFlag (/* in */ BMDDeckLinkConfigurationID cfgID, /* out */ bool *value) = 0;
virtual HRESULT SetInt (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ int64_t value) = 0;
virtual HRESULT GetInt (/* in */ BMDDeckLinkConfigurationID cfgID, /* out */ int64_t *value) = 0;
virtual HRESULT SetFloat (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ double value) = 0;
virtual HRESULT GetFloat (/* in */ BMDDeckLinkConfigurationID cfgID, /* out */ double *value) = 0;
virtual HRESULT SetString (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ const char *value) = 0;
virtual HRESULT GetString (/* in */ BMDDeckLinkConfigurationID cfgID, /* out */ const char **value) = 0;
virtual HRESULT WriteConfigurationToPreferences (void) = 0;
protected:
virtual ~IDeckLinkConfiguration_v10_9 () {} // call Release method to drop reference count
};
#endif /* defined(BMD_DECKLINKAPICONFIGURATION_v10_9_H) */

View file

@ -0,0 +1,84 @@
/* -LICENSE-START-
** Copyright (c) 2026 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation (the
** "Software") to use, reproduce, display, distribute, sub-license, execute,
** and transmit the Software, and to prepare derivative works of the Software,
** and to permit third-parties to whom the Software is furnished to do so, in
** accordance with:
**
** (1) if the Software is obtained from Blackmagic Design, the End User License
** Agreement for the Software Development Kit ("EULA") available at
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
**
** (2) if the Software is obtained from any third party, such licensing terms
** as notified by that third party,
**
** and all subject to the following:
**
** (3) the copyright notices in the Software and this entire statement,
** including the above license grant, this restriction and the following
** disclaimer, must be included in all copies of the Software, in whole or in
** part, and all derivative works of the Software, unless such copies or
** derivative works are solely in the form of machine-executable object code
** generated by a source language processor.
**
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
**
** A copy of the Software is available free of charge at
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
**
** -LICENSE-END-
*/
#pragma once
#include "DeckLinkAPIConfiguration.h"
/* Enum BMDDeckLinkConfigurationID_v15_3_1 - DeckLink Configuration ID */
typedef uint32_t BMDDeckLinkConfigurationID_v15_3_1;
enum _BMDDeckLinkConfigurationID_v15_3_1
{
/* Network Flags */
bmdDeckLinkConfigEthernetUseDHCP_v15_3_1 = /* 'DHCP' */ 0x44484350,
/* Network Strings */
bmdDeckLinkConfigEthernetStaticLocalIPAddress_v15_3_1 = /* 'nsip' */ 0x6E736970,
bmdDeckLinkConfigEthernetStaticSubnetMask_v15_3_1 = /* 'nssm' */ 0x6E73736D,
bmdDeckLinkConfigEthernetStaticGatewayIPAddress_v15_3_1 = /* 'nsgw' */ 0x6E736777,
bmdDeckLinkConfigEthernetStaticPrimaryDNS_v15_3_1 = /* 'nspd' */ 0x6E737064,
bmdDeckLinkConfigEthernetStaticSecondaryDNS_v15_3_1 = /* 'nssd' */ 0x6E737364,
bmdDeckLinkConfigEthernetVideoOutputAddress_v15_3_1 = /* 'noav' */ 0x6E6F6176,
bmdDeckLinkConfigEthernetAudioOutputAddress_v15_3_1 = /* 'noaa' */ 0x6E6F6161,
bmdDeckLinkConfigEthernetAncillaryOutputAddress_v15_3_1 = /* 'noaA' */ 0x6E6F6141,
};
// Interface ID Declarations
BMD_CONST REFIID IID_IDeckLinkConfiguration_v15_3_1 = /* 912F634B-2D4E-40A4-8AAB-8D80B73F1289 */ {0x91,0x2F,0x63,0x4B,0x2D,0x4E,0x40,0xA4,0x8A,0xAB,0x8D,0x80,0xB7,0x3F,0x12,0x89};
/* Interface IDeckLinkConfiguration_v15_3_1 - DeckLink Configuration interface */
class IDeckLinkConfiguration_v15_3_1 : public IUnknown
{
public:
virtual HRESULT SetFlag (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ bool value) = 0;
virtual HRESULT GetFlag (/* in */ BMDDeckLinkConfigurationID cfgID, /* out */ bool *value) = 0;
virtual HRESULT SetInt (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ int64_t value) = 0;
virtual HRESULT GetInt (/* in */ BMDDeckLinkConfigurationID cfgID, /* out */ int64_t *value) = 0;
virtual HRESULT SetFloat (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ double value) = 0;
virtual HRESULT GetFloat (/* in */ BMDDeckLinkConfigurationID cfgID, /* out */ double *value) = 0;
virtual HRESULT SetString (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ const char* value) = 0;
virtual HRESULT GetString (/* in */ BMDDeckLinkConfigurationID cfgID, /* out */ const char* *value) = 0;
virtual HRESULT WriteConfigurationToPreferences (void) = 0;
protected:
virtual ~IDeckLinkConfiguration_v15_3_1 () {} // call Release method to drop reference count
};

View file

@ -0,0 +1,227 @@
/* -LICENSE-START-
** Copyright (c) 2026 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation covered by
** this license (the "Software") to use, reproduce, display, distribute,
** execute, and transmit the Software, and to prepare derivative works of the
** Software, and to permit third-parties to whom the Software is furnished to
** do so, all subject to the following:
**
** The copyright notices in the Software and this entire statement, including
** the above license grant, this restriction and the following disclaimer,
** must be included in all copies of the Software, in whole or in part, and
** all derivative works of the Software, unless such copies or derivative
** works are solely in the form of machine-executable object code generated by
** a source language processor.
**
** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
** -LICENSE-END-
*/
/*
* -- AUTOMATICALLY GENERATED - DO NOT EDIT ---
*/
#ifndef BMD_DECKLINKAPIDECKCONTROL_H
#define BMD_DECKLINKAPIDECKCONTROL_H
#ifndef BMD_CONST
#if defined(_MSC_VER)
#define BMD_CONST __declspec(selectany) static const
#else
#define BMD_CONST static const
#endif
#endif
#ifndef BMD_PUBLIC
#define BMD_PUBLIC
#endif
// Type Declarations
// Interface ID Declarations
BMD_CONST REFIID IID_IDeckLinkDeckControlStatusCallback = /* 53436FFB-B434-4906-BADC-AE3060FFE8EF */ { 0x53,0x43,0x6F,0xFB,0xB4,0x34,0x49,0x06,0xBA,0xDC,0xAE,0x30,0x60,0xFF,0xE8,0xEF };
BMD_CONST REFIID IID_IDeckLinkDeckControl = /* 8E1C3ACE-19C7-4E00-8B92-D80431D958BE */ { 0x8E,0x1C,0x3A,0xCE,0x19,0xC7,0x4E,0x00,0x8B,0x92,0xD8,0x04,0x31,0xD9,0x58,0xBE };
/* Enum BMDDeckControlMode - DeckControl mode */
typedef uint32_t BMDDeckControlMode;
enum _BMDDeckControlMode {
bmdDeckControlNotOpened = /* 'ntop' */ 0x6E746F70,
bmdDeckControlVTRControlMode = /* 'vtrc' */ 0x76747263,
bmdDeckControlExportMode = /* 'expm' */ 0x6578706D,
bmdDeckControlCaptureMode = /* 'capm' */ 0x6361706D
};
/* Enum BMDDeckControlEvent - DeckControl event */
typedef uint32_t BMDDeckControlEvent;
enum _BMDDeckControlEvent {
bmdDeckControlAbortedEvent = /* 'abte' */ 0x61627465, // This event is triggered when a capture or edit-to-tape operation is aborted.
/* Export-To-Tape events */
bmdDeckControlPrepareForExportEvent = /* 'pfee' */ 0x70666565, // This event is triggered a few frames before reaching the in-point. IDeckLinkInput::StartScheduledPlayback should be called at this point.
bmdDeckControlExportCompleteEvent = /* 'exce' */ 0x65786365, // This event is triggered a few frames after reaching the out-point. At this point, it is safe to stop playback. Upon reception of this event the deck's control mode is set back to bmdDeckControlVTRControlMode.
/* Capture events */
bmdDeckControlPrepareForCaptureEvent = /* 'pfce' */ 0x70666365, // This event is triggered a few frames before reaching the in-point. The serial timecode attached to IDeckLinkVideoInputFrames is now valid.
bmdDeckControlCaptureCompleteEvent = /* 'ccev' */ 0x63636576 // This event is triggered a few frames after reaching the out-point. Upon reception of this event the deck's control mode is set back to bmdDeckControlVTRControlMode.
};
/* Enum BMDDeckControlVTRControlState - VTR Control state */
typedef uint32_t BMDDeckControlVTRControlState;
enum _BMDDeckControlVTRControlState {
bmdDeckControlNotInVTRControlMode = /* 'nvcm' */ 0x6E76636D,
bmdDeckControlVTRControlPlaying = /* 'vtrp' */ 0x76747270,
bmdDeckControlVTRControlRecording = /* 'vtrr' */ 0x76747272,
bmdDeckControlVTRControlStill = /* 'vtra' */ 0x76747261,
bmdDeckControlVTRControlShuttleForward = /* 'vtsf' */ 0x76747366,
bmdDeckControlVTRControlShuttleReverse = /* 'vtsr' */ 0x76747372,
bmdDeckControlVTRControlJogForward = /* 'vtjf' */ 0x76746A66,
bmdDeckControlVTRControlJogReverse = /* 'vtjr' */ 0x76746A72,
bmdDeckControlVTRControlStopped = /* 'vtro' */ 0x7674726F
};
/* Enum BMDDeckControlStatusFlags - Deck Control status flags */
typedef uint32_t BMDDeckControlStatusFlags;
enum _BMDDeckControlStatusFlags {
bmdDeckControlStatusDeckConnected = 1 << 0,
bmdDeckControlStatusRemoteMode = 1 << 1,
bmdDeckControlStatusRecordInhibited = 1 << 2,
bmdDeckControlStatusCassetteOut = 1 << 3
};
/* Enum BMDDeckControlExportModeOpsFlags - Export mode flags */
typedef uint32_t BMDDeckControlExportModeOpsFlags;
enum _BMDDeckControlExportModeOpsFlags {
bmdDeckControlExportModeInsertVideo = 1 << 0,
bmdDeckControlExportModeInsertAudio1 = 1 << 1,
bmdDeckControlExportModeInsertAudio2 = 1 << 2,
bmdDeckControlExportModeInsertAudio3 = 1 << 3,
bmdDeckControlExportModeInsertAudio4 = 1 << 4,
bmdDeckControlExportModeInsertAudio5 = 1 << 5,
bmdDeckControlExportModeInsertAudio6 = 1 << 6,
bmdDeckControlExportModeInsertAudio7 = 1 << 7,
bmdDeckControlExportModeInsertAudio8 = 1 << 8,
bmdDeckControlExportModeInsertAudio9 = 1 << 9,
bmdDeckControlExportModeInsertAudio10 = 1 << 10,
bmdDeckControlExportModeInsertAudio11 = 1 << 11,
bmdDeckControlExportModeInsertAudio12 = 1 << 12,
bmdDeckControlExportModeInsertTimeCode = 1 << 13,
bmdDeckControlExportModeInsertAssemble = 1 << 14,
bmdDeckControlExportModeInsertPreview = 1 << 15,
bmdDeckControlUseManualExport = 1 << 16
};
/* Enum BMDDeckControlError - Deck Control error */
typedef uint32_t BMDDeckControlError;
enum _BMDDeckControlError {
bmdDeckControlNoError = /* 'noer' */ 0x6E6F6572,
bmdDeckControlModeError = /* 'moer' */ 0x6D6F6572,
bmdDeckControlMissedInPointError = /* 'mier' */ 0x6D696572,
bmdDeckControlDeckTimeoutError = /* 'dter' */ 0x64746572,
bmdDeckControlCommandFailedError = /* 'cfer' */ 0x63666572,
bmdDeckControlDeviceAlreadyOpenedError = /* 'dalo' */ 0x64616C6F,
bmdDeckControlFailedToOpenDeviceError = /* 'fder' */ 0x66646572,
bmdDeckControlInLocalModeError = /* 'lmer' */ 0x6C6D6572,
bmdDeckControlEndOfTapeError = /* 'eter' */ 0x65746572,
bmdDeckControlUserAbortError = /* 'uaer' */ 0x75616572,
bmdDeckControlNoTapeInDeckError = /* 'nter' */ 0x6E746572,
bmdDeckControlNoVideoFromCardError = /* 'nvfc' */ 0x6E766663,
bmdDeckControlNoCommunicationError = /* 'ncom' */ 0x6E636F6D,
bmdDeckControlBufferTooSmallError = /* 'btsm' */ 0x6274736D,
bmdDeckControlBadChecksumError = /* 'chks' */ 0x63686B73,
bmdDeckControlUnknownError = /* 'uner' */ 0x756E6572
};
#if defined(__cplusplus)
// Forward Declarations
class IDeckLinkDeckControlStatusCallback;
class IDeckLinkDeckControl;
/* Interface IDeckLinkDeckControlStatusCallback - Deck control state change callback. */
class BMD_PUBLIC IDeckLinkDeckControlStatusCallback : public IUnknown
{
public:
virtual HRESULT TimecodeUpdate (/* in */ BMDTimecodeBCD currentTimecode) = 0;
virtual HRESULT VTRControlStateChanged (/* in */ BMDDeckControlVTRControlState newState, /* in */ BMDDeckControlError error) = 0;
virtual HRESULT DeckControlEventReceived (/* in */ BMDDeckControlEvent event, /* in */ BMDDeckControlError error) = 0;
virtual HRESULT DeckControlStatusChanged (/* in */ BMDDeckControlStatusFlags flags, /* in */ uint32_t mask) = 0;
protected:
virtual ~IDeckLinkDeckControlStatusCallback () {} // call Release method to drop reference count
};
/* Interface IDeckLinkDeckControl - Deck Control main interface */
class BMD_PUBLIC IDeckLinkDeckControl : public IUnknown
{
public:
virtual HRESULT Open (/* in */ BMDTimeScale timeScale, /* in */ BMDTimeValue timeValue, /* in */ bool timecodeIsDropFrame, /* out */ BMDDeckControlError* error) = 0;
virtual HRESULT Close (/* in */ bool standbyOn) = 0;
virtual HRESULT GetCurrentState (/* out */ BMDDeckControlMode* mode, /* out */ BMDDeckControlVTRControlState* vtrControlState, /* out */ BMDDeckControlStatusFlags* flags) = 0;
virtual HRESULT SetStandby (/* in */ bool standbyOn) = 0;
virtual HRESULT SendCommand (/* in */ uint8_t* inBuffer, /* in */ uint32_t inBufferSize, /* out */ uint8_t* outBuffer, /* out */ uint32_t* outDataSize, /* in */ uint32_t outBufferSize, /* out */ BMDDeckControlError* error) = 0;
virtual HRESULT Play (/* out */ BMDDeckControlError* error) = 0;
virtual HRESULT Stop (/* out */ BMDDeckControlError* error) = 0;
virtual HRESULT TogglePlayStop (/* out */ BMDDeckControlError* error) = 0;
virtual HRESULT Eject (/* out */ BMDDeckControlError* error) = 0;
virtual HRESULT GoToTimecode (/* in */ BMDTimecodeBCD timecode, /* out */ BMDDeckControlError* error) = 0;
virtual HRESULT FastForward (/* in */ bool viewTape, /* out */ BMDDeckControlError* error) = 0;
virtual HRESULT Rewind (/* in */ bool viewTape, /* out */ BMDDeckControlError* error) = 0;
virtual HRESULT StepForward (/* out */ BMDDeckControlError* error) = 0;
virtual HRESULT StepBack (/* out */ BMDDeckControlError* error) = 0;
virtual HRESULT Jog (/* in */ double rate, /* out */ BMDDeckControlError* error) = 0;
virtual HRESULT Shuttle (/* in */ double rate, /* out */ BMDDeckControlError* error) = 0;
virtual HRESULT GetTimecodeString (/* out */ const char** currentTimeCode, /* out */ BMDDeckControlError* error) = 0;
virtual HRESULT GetTimecode (/* out */ IDeckLinkTimecode** currentTimecode, /* out */ BMDDeckControlError* error) = 0;
virtual HRESULT GetTimecodeBCD (/* out */ BMDTimecodeBCD* currentTimecode, /* out */ BMDDeckControlError* error) = 0;
virtual HRESULT SetPreroll (/* in */ uint32_t prerollSeconds) = 0;
virtual HRESULT GetPreroll (/* out */ uint32_t* prerollSeconds) = 0;
virtual HRESULT SetExportOffset (/* in */ int32_t exportOffsetFields) = 0;
virtual HRESULT GetExportOffset (/* out */ int32_t* exportOffsetFields) = 0;
virtual HRESULT GetManualExportOffset (/* out */ int32_t* deckManualExportOffsetFields) = 0;
virtual HRESULT SetCaptureOffset (/* in */ int32_t captureOffsetFields) = 0;
virtual HRESULT GetCaptureOffset (/* out */ int32_t* captureOffsetFields) = 0;
virtual HRESULT StartExport (/* in */ BMDTimecodeBCD inTimecode, /* in */ BMDTimecodeBCD outTimecode, /* in */ BMDDeckControlExportModeOpsFlags exportModeOps, /* out */ BMDDeckControlError* error) = 0;
virtual HRESULT StartCapture (/* in */ bool useVITC, /* in */ BMDTimecodeBCD inTimecode, /* in */ BMDTimecodeBCD outTimecode, /* out */ BMDDeckControlError* error) = 0;
virtual HRESULT GetDeviceID (/* out */ uint16_t* deviceId, /* out */ BMDDeckControlError* error) = 0;
virtual HRESULT Abort (void) = 0;
virtual HRESULT CrashRecordStart (/* out */ BMDDeckControlError* error) = 0;
virtual HRESULT CrashRecordStop (/* out */ BMDDeckControlError* error) = 0;
virtual HRESULT SetCallback (/* in */ IDeckLinkDeckControlStatusCallback* callback) = 0;
protected:
virtual ~IDeckLinkDeckControl () {} // call Release method to drop reference count
};
/* Functions */
extern "C" {
}
#endif /* defined(__cplusplus) */
#endif /* defined(BMD_DECKLINKAPIDECKCONTROL_H) */

View file

@ -0,0 +1,83 @@
/* -LICENSE-START-
** Copyright (c) 2026 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation covered by
** this license (the "Software") to use, reproduce, display, distribute,
** execute, and transmit the Software, and to prepare derivative works of the
** Software, and to permit third-parties to whom the Software is furnished to
** do so, all subject to the following:
**
** The copyright notices in the Software and this entire statement, including
** the above license grant, this restriction and the following disclaimer,
** must be included in all copies of the Software, in whole or in part, and
** all derivative works of the Software, unless such copies or derivative
** works are solely in the form of machine-executable object code generated by
** a source language processor.
**
** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
** -LICENSE-END-
*/
/*
* -- AUTOMATICALLY GENERATED - DO NOT EDIT ---
*/
#ifndef BMD_DECKLINKAPIDISCOVERY_H
#define BMD_DECKLINKAPIDISCOVERY_H
#ifndef BMD_CONST
#if defined(_MSC_VER)
#define BMD_CONST __declspec(selectany) static const
#else
#define BMD_CONST static const
#endif
#endif
#ifndef BMD_PUBLIC
#define BMD_PUBLIC
#endif
// Type Declarations
// Interface ID Declarations
BMD_CONST REFIID IID_IDeckLink = /* C418FBDD-0587-48ED-8FE5-640F0A14AF91 */ { 0xC4,0x18,0xFB,0xDD,0x05,0x87,0x48,0xED,0x8F,0xE5,0x64,0x0F,0x0A,0x14,0xAF,0x91 };
#if defined(__cplusplus)
// Forward Declarations
class IDeckLink;
/* Interface IDeckLink - Represents a DeckLink device */
class BMD_PUBLIC IDeckLink : public IUnknown
{
public:
virtual HRESULT GetModelName (/* out */ const char** modelName) = 0;
virtual HRESULT GetDisplayName (/* out */ const char** displayName) = 0;
protected:
virtual ~IDeckLink () {} // call Release method to drop reference count
};
/* Functions */
extern "C" {
}
#endif /* defined(__cplusplus) */
#endif /* defined(BMD_DECKLINKAPIDISCOVERY_H) */

View file

@ -0,0 +1,188 @@
/* -LICENSE-START-
** Copyright (c) 2009 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation (the
** "Software") to use, reproduce, display, distribute, sub-license, execute,
** and transmit the Software, and to prepare derivative works of the Software,
** and to permit third-parties to whom the Software is furnished to do so, in
** accordance with:
**
** (1) if the Software is obtained from Blackmagic Design, the End User License
** Agreement for the Software Development Kit ("EULA") available at
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
**
** (2) if the Software is obtained from any third party, such licensing terms
** as notified by that third party,
**
** and all subject to the following:
**
** (3) the copyright notices in the Software and this entire statement,
** including the above license grant, this restriction and the following
** disclaimer, must be included in all copies of the Software, in whole or in
** part, and all derivative works of the Software, unless such copies or
** derivative works are solely in the form of machine-executable object code
** generated by a source language processor.
**
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
**
** A copy of the Software is available free of charge at
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
**
** -LICENSE-END-
**/
#include <stdio.h>
#include <pthread.h>
#include <dlfcn.h>
#include "DeckLinkAPI.h"
#define kDeckLinkAPI_Name "libDeckLinkAPI.so"
#define KDeckLinkPreviewAPI_Name "libDeckLinkPreviewAPI.so"
typedef IDeckLinkIterator* (*CreateIteratorFunc)(void);
typedef IDeckLinkAPIInformation* (*CreateAPIInformationFunc)(void);
typedef IDeckLinkGLScreenPreviewHelper* (*CreateOpenGLScreenPreviewHelperFunc)(void);
typedef IDeckLinkGLScreenPreviewHelper* (*CreateOpenGL3ScreenPreviewHelperFunc)(void);
typedef IDeckLinkVideoConversion* (*CreateVideoConversionInstanceFunc)(void);
typedef IDeckLinkDiscovery* (*CreateDeckLinkDiscoveryInstanceFunc)(void);
typedef IDeckLinkVideoFrameAncillaryPackets* (*CreateVideoFrameAncillaryPacketsInstanceFunc)(void);
static pthread_once_t gDeckLinkOnceControl = PTHREAD_ONCE_INIT;
static pthread_once_t gPreviewOnceControl = PTHREAD_ONCE_INIT;
static bool gLoadedDeckLinkAPI = false;
static CreateIteratorFunc gCreateIteratorFunc = NULL;
static CreateAPIInformationFunc gCreateAPIInformationFunc = NULL;
static CreateOpenGLScreenPreviewHelperFunc gCreateOpenGLPreviewFunc = NULL;
static CreateOpenGL3ScreenPreviewHelperFunc gCreateOpenGL3PreviewFunc = NULL;
static CreateVideoConversionInstanceFunc gCreateVideoConversionFunc = NULL;
static CreateDeckLinkDiscoveryInstanceFunc gCreateDeckLinkDiscoveryFunc = NULL;
static CreateVideoFrameAncillaryPacketsInstanceFunc gCreateVideoFrameAncillaryPacketsFunc = NULL;
static void InitDeckLinkAPI (void)
{
void *libraryHandle;
libraryHandle = dlopen(kDeckLinkAPI_Name, RTLD_NOW|RTLD_GLOBAL);
if (!libraryHandle)
{
fprintf(stderr, "%s\n", dlerror());
return;
}
gLoadedDeckLinkAPI = true;
gCreateIteratorFunc = (CreateIteratorFunc)dlsym(libraryHandle, "CreateDeckLinkIteratorInstance_0004");
if (!gCreateIteratorFunc)
fprintf(stderr, "%s\n", dlerror());
gCreateAPIInformationFunc = (CreateAPIInformationFunc)dlsym(libraryHandle, "CreateDeckLinkAPIInformationInstance_0001");
if (!gCreateAPIInformationFunc)
fprintf(stderr, "%s\n", dlerror());
gCreateVideoConversionFunc = (CreateVideoConversionInstanceFunc)dlsym(libraryHandle, "CreateVideoConversionInstance_0003");
if (!gCreateVideoConversionFunc)
fprintf(stderr, "%s\n", dlerror());
gCreateDeckLinkDiscoveryFunc = (CreateDeckLinkDiscoveryInstanceFunc)dlsym(libraryHandle, "CreateDeckLinkDiscoveryInstance_0003");
if (!gCreateDeckLinkDiscoveryFunc)
fprintf(stderr, "%s\n", dlerror());
gCreateVideoFrameAncillaryPacketsFunc = (CreateVideoFrameAncillaryPacketsInstanceFunc)dlsym(libraryHandle, "CreateVideoFrameAncillaryPacketsInstance_0002");
if (!gCreateVideoFrameAncillaryPacketsFunc)
fprintf(stderr, "%s\n", dlerror());
}
static void InitDeckLinkPreviewAPI (void)
{
void *libraryHandle;
libraryHandle = dlopen(KDeckLinkPreviewAPI_Name, RTLD_NOW|RTLD_GLOBAL);
if (!libraryHandle)
{
fprintf(stderr, "%s\n", dlerror());
return;
}
gCreateOpenGLPreviewFunc = (CreateOpenGLScreenPreviewHelperFunc)dlsym(libraryHandle, "CreateOpenGLScreenPreviewHelper_0002");
if (!gCreateOpenGLPreviewFunc)
fprintf(stderr, "%s\n", dlerror());
gCreateOpenGL3PreviewFunc = (CreateOpenGL3ScreenPreviewHelperFunc)dlsym(libraryHandle, "CreateOpenGL3ScreenPreviewHelper_0002");
if (!gCreateOpenGL3PreviewFunc)
fprintf(stderr, "%s\n", dlerror());
}
bool IsDeckLinkAPIPresent (void)
{
// If the DeckLink API dynamic library was successfully loaded, return this knowledge to the caller
return gLoadedDeckLinkAPI;
}
IDeckLinkIterator* CreateDeckLinkIteratorInstance (void)
{
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
if (gCreateIteratorFunc == NULL)
return NULL;
return gCreateIteratorFunc();
}
IDeckLinkAPIInformation* CreateDeckLinkAPIInformationInstance (void)
{
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
if (gCreateAPIInformationFunc == NULL)
return NULL;
return gCreateAPIInformationFunc();
}
IDeckLinkGLScreenPreviewHelper* CreateOpenGLScreenPreviewHelper (void)
{
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
pthread_once(&gPreviewOnceControl, InitDeckLinkPreviewAPI);
if (gCreateOpenGLPreviewFunc == NULL)
return NULL;
return gCreateOpenGLPreviewFunc();
}
IDeckLinkGLScreenPreviewHelper* CreateOpenGL3ScreenPreviewHelper (void)
{
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
pthread_once(&gPreviewOnceControl, InitDeckLinkPreviewAPI);
if (gCreateOpenGL3PreviewFunc == NULL)
return NULL;
return gCreateOpenGL3PreviewFunc();
}
IDeckLinkVideoConversion* CreateVideoConversionInstance (void)
{
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
if (gCreateVideoConversionFunc == NULL)
return NULL;
return gCreateVideoConversionFunc();
}
IDeckLinkDiscovery* CreateDeckLinkDiscoveryInstance (void)
{
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
if (gCreateDeckLinkDiscoveryFunc == NULL)
return NULL;
return gCreateDeckLinkDiscoveryFunc();
}
IDeckLinkVideoFrameAncillaryPackets* CreateVideoFrameAncillaryPacketsInstance (void)
{
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
if (gCreateVideoFrameAncillaryPacketsFunc == NULL)
return NULL;
return gCreateVideoFrameAncillaryPacketsFunc();
}

View file

@ -0,0 +1,173 @@
/* -LICENSE-START-
** Copyright (c) 2019 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation (the
** "Software") to use, reproduce, display, distribute, sub-license, execute,
** and transmit the Software, and to prepare derivative works of the Software,
** and to permit third-parties to whom the Software is furnished to do so, in
** accordance with:
**
** (1) if the Software is obtained from Blackmagic Design, the End User License
** Agreement for the Software Development Kit ("EULA") available at
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
**
** (2) if the Software is obtained from any third party, such licensing terms
** as notified by that third party,
**
** and all subject to the following:
**
** (3) the copyright notices in the Software and this entire statement,
** including the above license grant, this restriction and the following
** disclaimer, must be included in all copies of the Software, in whole or in
** part, and all derivative works of the Software, unless such copies or
** derivative works are solely in the form of machine-executable object code
** generated by a source language processor.
**
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
**
** A copy of the Software is available free of charge at
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
**
** -LICENSE-END-
**/
#include <stdio.h>
#include <pthread.h>
#include <dlfcn.h>
#include "DeckLinkAPI_v10_11.h"
#define kDeckLinkAPI_Name "libDeckLinkAPI.so"
#define KDeckLinkPreviewAPI_Name "libDeckLinkPreviewAPI.so"
typedef IDeckLinkIterator* (*CreateIteratorFunc)(void);
typedef IDeckLinkAPIInformation* (*CreateAPIInformationFunc)(void);
typedef IDeckLinkGLScreenPreviewHelper* (*CreateOpenGLScreenPreviewHelperFunc)(void);
typedef IDeckLinkVideoConversion* (*CreateVideoConversionInstanceFunc)(void);
typedef IDeckLinkDiscovery* (*CreateDeckLinkDiscoveryInstanceFunc)(void);
typedef IDeckLinkVideoFrameAncillaryPackets* (*CreateVideoFrameAncillaryPacketsInstanceFunc)(void);
static pthread_once_t gDeckLinkOnceControl = PTHREAD_ONCE_INIT;
static pthread_once_t gPreviewOnceControl = PTHREAD_ONCE_INIT;
static bool gLoadedDeckLinkAPI = false;
static CreateIteratorFunc gCreateIteratorFunc = NULL;
static CreateAPIInformationFunc gCreateAPIInformationFunc = NULL;
static CreateOpenGLScreenPreviewHelperFunc gCreateOpenGLPreviewFunc = NULL;
static CreateVideoConversionInstanceFunc gCreateVideoConversionFunc = NULL;
static CreateDeckLinkDiscoveryInstanceFunc gCreateDeckLinkDiscoveryFunc = NULL;
static CreateVideoFrameAncillaryPacketsInstanceFunc gCreateVideoFrameAncillaryPacketsFunc = NULL;
static void InitDeckLinkAPI (void)
{
void *libraryHandle;
libraryHandle = dlopen(kDeckLinkAPI_Name, RTLD_NOW|RTLD_GLOBAL);
if (!libraryHandle)
{
fprintf(stderr, "%s\n", dlerror());
return;
}
gLoadedDeckLinkAPI = true;
gCreateIteratorFunc = (CreateIteratorFunc)dlsym(libraryHandle, "CreateDeckLinkIteratorInstance_0003");
if (!gCreateIteratorFunc)
fprintf(stderr, "%s\n", dlerror());
gCreateAPIInformationFunc = (CreateAPIInformationFunc)dlsym(libraryHandle, "CreateDeckLinkAPIInformationInstance_0001");
if (!gCreateAPIInformationFunc)
fprintf(stderr, "%s\n", dlerror());
gCreateVideoConversionFunc = (CreateVideoConversionInstanceFunc)dlsym(libraryHandle, "CreateVideoConversionInstance_0001");
if (!gCreateVideoConversionFunc)
fprintf(stderr, "%s\n", dlerror());
gCreateDeckLinkDiscoveryFunc = (CreateDeckLinkDiscoveryInstanceFunc)dlsym(libraryHandle, "CreateDeckLinkDiscoveryInstance_0002");
if (!gCreateDeckLinkDiscoveryFunc)
fprintf(stderr, "%s\n", dlerror());
gCreateVideoFrameAncillaryPacketsFunc = (CreateVideoFrameAncillaryPacketsInstanceFunc)dlsym(libraryHandle, "CreateVideoFrameAncillaryPacketsInstance_0001");
if (!gCreateVideoFrameAncillaryPacketsFunc)
fprintf(stderr, "%s\n", dlerror());
}
static void InitDeckLinkPreviewAPI (void)
{
void *libraryHandle;
libraryHandle = dlopen(KDeckLinkPreviewAPI_Name, RTLD_NOW|RTLD_GLOBAL);
if (!libraryHandle)
{
fprintf(stderr, "%s\n", dlerror());
return;
}
gCreateOpenGLPreviewFunc = (CreateOpenGLScreenPreviewHelperFunc)dlsym(libraryHandle, "CreateOpenGLScreenPreviewHelper_0001");
if (!gCreateOpenGLPreviewFunc)
fprintf(stderr, "%s\n", dlerror());
}
bool IsDeckLinkAPIPresent_v10_11 (void)
{
// If the DeckLink API dynamic library was successfully loaded, return this knowledge to the caller
return gLoadedDeckLinkAPI;
}
IDeckLinkIterator* CreateDeckLinkIteratorInstance_v10_11 (void)
{
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
if (gCreateIteratorFunc == NULL)
return NULL;
return gCreateIteratorFunc();
}
IDeckLinkAPIInformation* CreateDeckLinkAPIInformationInstance_v10_11 (void)
{
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
if (gCreateAPIInformationFunc == NULL)
return NULL;
return gCreateAPIInformationFunc();
}
IDeckLinkGLScreenPreviewHelper* CreateOpenGLScreenPreviewHelper_v10_11 (void)
{
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
pthread_once(&gPreviewOnceControl, InitDeckLinkPreviewAPI);
if (gCreateOpenGLPreviewFunc == NULL)
return NULL;
return gCreateOpenGLPreviewFunc();
}
IDeckLinkVideoConversion* CreateVideoConversionInstance_v10_11 (void)
{
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
if (gCreateVideoConversionFunc == NULL)
return NULL;
return gCreateVideoConversionFunc();
}
IDeckLinkDiscovery* CreateDeckLinkDiscoveryInstance_v10_11 (void)
{
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
if (gCreateDeckLinkDiscoveryFunc == NULL)
return NULL;
return gCreateDeckLinkDiscoveryFunc();
}
IDeckLinkVideoFrameAncillaryPackets* CreateVideoFrameAncillaryPacketsInstance_v10_11 (void)
{
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
if (gCreateVideoFrameAncillaryPacketsFunc == NULL)
return NULL;
return gCreateVideoFrameAncillaryPacketsFunc();
}

View file

@ -0,0 +1,159 @@
/* -LICENSE-START-
** Copyright (c) 2009 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation (the
** "Software") to use, reproduce, display, distribute, sub-license, execute,
** and transmit the Software, and to prepare derivative works of the Software,
** and to permit third-parties to whom the Software is furnished to do so, in
** accordance with:
**
** (1) if the Software is obtained from Blackmagic Design, the End User License
** Agreement for the Software Development Kit ("EULA") available at
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
**
** (2) if the Software is obtained from any third party, such licensing terms
** as notified by that third party,
**
** and all subject to the following:
**
** (3) the copyright notices in the Software and this entire statement,
** including the above license grant, this restriction and the following
** disclaimer, must be included in all copies of the Software, in whole or in
** part, and all derivative works of the Software, unless such copies or
** derivative works are solely in the form of machine-executable object code
** generated by a source language processor.
**
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
**
** A copy of the Software is available free of charge at
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
**
** -LICENSE-END-
**/
#include <stdio.h>
#include <pthread.h>
#include <dlfcn.h>
#include "DeckLinkAPI.h"
#define kDeckLinkAPI_Name "libDeckLinkAPI.so"
#define KDeckLinkPreviewAPI_Name "libDeckLinkPreviewAPI.so"
typedef IDeckLinkIterator* (*CreateIteratorFunc)(void);
typedef IDeckLinkAPIInformation* (*CreateAPIInformationFunc)(void);
typedef IDeckLinkGLScreenPreviewHelper* (*CreateOpenGLScreenPreviewHelperFunc)(void);
typedef IDeckLinkVideoConversion* (*CreateVideoConversionInstanceFunc)(void);
typedef IDeckLinkDiscovery* (*CreateDeckLinkDiscoveryInstanceFunc)(void);
static pthread_once_t gDeckLinkOnceControl = PTHREAD_ONCE_INIT;
static pthread_once_t gPreviewOnceControl = PTHREAD_ONCE_INIT;
static bool gLoadedDeckLinkAPI = false;
static CreateIteratorFunc gCreateIteratorFunc = NULL;
static CreateAPIInformationFunc gCreateAPIInformationFunc = NULL;
static CreateOpenGLScreenPreviewHelperFunc gCreateOpenGLPreviewFunc = NULL;
static CreateVideoConversionInstanceFunc gCreateVideoConversionFunc = NULL;
static CreateDeckLinkDiscoveryInstanceFunc gCreateDeckLinkDiscoveryFunc = NULL;
static void InitDeckLinkAPI(void)
{
void *libraryHandle;
libraryHandle = dlopen(kDeckLinkAPI_Name, RTLD_NOW | RTLD_GLOBAL);
if (!libraryHandle)
{
fprintf(stderr, "%s\n", dlerror());
return;
}
gLoadedDeckLinkAPI = true;
gCreateIteratorFunc = (CreateIteratorFunc)dlsym(libraryHandle, "CreateDeckLinkIteratorInstance_0002");
if (!gCreateIteratorFunc)
fprintf(stderr, "%s\n", dlerror());
gCreateAPIInformationFunc = (CreateAPIInformationFunc)dlsym(libraryHandle, "CreateDeckLinkAPIInformationInstance_0001");
if (!gCreateAPIInformationFunc)
fprintf(stderr, "%s\n", dlerror());
gCreateVideoConversionFunc = (CreateVideoConversionInstanceFunc)dlsym(libraryHandle, "CreateVideoConversionInstance_0001");
if (!gCreateVideoConversionFunc)
fprintf(stderr, "%s\n", dlerror());
gCreateDeckLinkDiscoveryFunc = (CreateDeckLinkDiscoveryInstanceFunc)dlsym(libraryHandle, "CreateDeckLinkDiscoveryInstance_0001");
if (!gCreateDeckLinkDiscoveryFunc)
fprintf(stderr, "%s\n", dlerror());
}
static void InitDeckLinkPreviewAPI(void)
{
void *libraryHandle;
libraryHandle = dlopen(KDeckLinkPreviewAPI_Name, RTLD_NOW | RTLD_GLOBAL);
if (!libraryHandle)
{
fprintf(stderr, "%s\n", dlerror());
return;
}
gCreateOpenGLPreviewFunc = (CreateOpenGLScreenPreviewHelperFunc)dlsym(libraryHandle, "CreateOpenGLScreenPreviewHelper_0001");
if (!gCreateOpenGLPreviewFunc)
fprintf(stderr, "%s\n", dlerror());
}
bool IsDeckLinkAPIPresent(void)
{
// If the DeckLink API dynamic library was successfully loaded, return this knowledge to the caller
return gLoadedDeckLinkAPI;
}
IDeckLinkIterator* CreateDeckLinkIteratorInstance(void)
{
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
if (gCreateIteratorFunc == NULL)
return NULL;
return gCreateIteratorFunc();
}
IDeckLinkAPIInformation* CreateDeckLinkAPIInformationInstance(void)
{
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
if (gCreateAPIInformationFunc == NULL)
return NULL;
return gCreateAPIInformationFunc();
}
IDeckLinkGLScreenPreviewHelper* CreateOpenGLScreenPreviewHelper(void)
{
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
pthread_once(&gPreviewOnceControl, InitDeckLinkPreviewAPI);
if (gCreateOpenGLPreviewFunc == NULL)
return NULL;
return gCreateOpenGLPreviewFunc();
}
IDeckLinkVideoConversion* CreateVideoConversionInstance(void)
{
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
if (gCreateVideoConversionFunc == NULL)
return NULL;
return gCreateVideoConversionFunc();
}
IDeckLinkDiscovery* CreateDeckLinkDiscoveryInstance(void)
{
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
if (gCreateDeckLinkDiscoveryFunc == NULL)
return NULL;
return gCreateDeckLinkDiscoveryFunc();
}

View file

@ -0,0 +1,188 @@
/* -LICENSE-START-
** Copyright (c) 2009 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation (the
** "Software") to use, reproduce, display, distribute, sub-license, execute,
** and transmit the Software, and to prepare derivative works of the Software,
** and to permit third-parties to whom the Software is furnished to do so, in
** accordance with:
**
** (1) if the Software is obtained from Blackmagic Design, the End User License
** Agreement for the Software Development Kit (EULA) available at
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
**
** (2) if the Software is obtained from any third party, such licensing terms
** as notified by that third party,
**
** and all subject to the following:
**
** (3) the copyright notices in the Software and this entire statement,
** including the above license grant, this restriction and the following
** disclaimer, must be included in all copies of the Software, in whole or in
** part, and all derivative works of the Software, unless such copies or
** derivative works are solely in the form of machine-executable object code
** generated by a source language processor.
**
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
**
** A copy of the Software is available free of charge at
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
**
** -LICENSE-END-
**/
#include <stdio.h>
#include <pthread.h>
#include <dlfcn.h>
#include "DeckLinkAPI_v14_2_1.h"
#define kDeckLinkAPI_Name "libDeckLinkAPI.so"
#define KDeckLinkPreviewAPI_Name "libDeckLinkPreviewAPI.so"
typedef IDeckLinkIterator* (*CreateIteratorFunc)(void);
typedef IDeckLinkAPIInformation* (*CreateAPIInformationFunc)(void);
typedef IDeckLinkGLScreenPreviewHelper_v14_2_1* (*CreateOpenGLScreenPreviewHelperFunc)(void);
typedef IDeckLinkGLScreenPreviewHelper_v14_2_1* (*CreateOpenGL3ScreenPreviewHelperFunc)(void);
typedef IDeckLinkVideoConversion_v14_2_1* (*CreateVideoConversionInstanceFunc)(void);
typedef IDeckLinkDiscovery* (*CreateDeckLinkDiscoveryInstanceFunc)(void);
typedef IDeckLinkVideoFrameAncillaryPackets* (*CreateVideoFrameAncillaryPacketsInstanceFunc)(void);
static pthread_once_t gDeckLinkOnceControl = PTHREAD_ONCE_INIT;
static pthread_once_t gPreviewOnceControl = PTHREAD_ONCE_INIT;
static bool gLoadedDeckLinkAPI = false;
static CreateIteratorFunc gCreateIteratorFunc = NULL;
static CreateAPIInformationFunc gCreateAPIInformationFunc = NULL;
static CreateOpenGLScreenPreviewHelperFunc gCreateOpenGLPreviewFunc = NULL;
static CreateOpenGL3ScreenPreviewHelperFunc gCreateOpenGL3PreviewFunc = NULL;
static CreateVideoConversionInstanceFunc gCreateVideoConversionFunc = NULL;
static CreateDeckLinkDiscoveryInstanceFunc gCreateDeckLinkDiscoveryFunc = NULL;
static CreateVideoFrameAncillaryPacketsInstanceFunc gCreateVideoFrameAncillaryPacketsFunc = NULL;
static void InitDeckLinkAPI (void)
{
void *libraryHandle;
libraryHandle = dlopen(kDeckLinkAPI_Name, RTLD_NOW|RTLD_GLOBAL);
if (!libraryHandle)
{
fprintf(stderr, "%s\n", dlerror());
return;
}
gLoadedDeckLinkAPI = true;
gCreateIteratorFunc = (CreateIteratorFunc)dlsym(libraryHandle, "CreateDeckLinkIteratorInstance_0004");
if (!gCreateIteratorFunc)
fprintf(stderr, "%s\n", dlerror());
gCreateAPIInformationFunc = (CreateAPIInformationFunc)dlsym(libraryHandle, "CreateDeckLinkAPIInformationInstance_0001");
if (!gCreateAPIInformationFunc)
fprintf(stderr, "%s\n", dlerror());
gCreateVideoConversionFunc = (CreateVideoConversionInstanceFunc)dlsym(libraryHandle, "CreateVideoConversionInstance_0001");
if (!gCreateVideoConversionFunc)
fprintf(stderr, "%s\n", dlerror());
gCreateDeckLinkDiscoveryFunc = (CreateDeckLinkDiscoveryInstanceFunc)dlsym(libraryHandle, "CreateDeckLinkDiscoveryInstance_0003");
if (!gCreateDeckLinkDiscoveryFunc)
fprintf(stderr, "%s\n", dlerror());
gCreateVideoFrameAncillaryPacketsFunc = (CreateVideoFrameAncillaryPacketsInstanceFunc)dlsym(libraryHandle, "CreateVideoFrameAncillaryPacketsInstance_0001");
if (!gCreateVideoFrameAncillaryPacketsFunc)
fprintf(stderr, "%s\n", dlerror());
}
static void InitDeckLinkPreviewAPI (void)
{
void *libraryHandle;
libraryHandle = dlopen(KDeckLinkPreviewAPI_Name, RTLD_NOW|RTLD_GLOBAL);
if (!libraryHandle)
{
fprintf(stderr, "%s\n", dlerror());
return;
}
gCreateOpenGLPreviewFunc = (CreateOpenGLScreenPreviewHelperFunc)dlsym(libraryHandle, "CreateOpenGLScreenPreviewHelper_0001");
if (!gCreateOpenGLPreviewFunc)
fprintf(stderr, "%s\n", dlerror());
gCreateOpenGL3PreviewFunc = (CreateOpenGL3ScreenPreviewHelperFunc)dlsym(libraryHandle, "CreateOpenGL3ScreenPreviewHelper_0001");
if (!gCreateOpenGL3PreviewFunc)
fprintf(stderr, "%s\n", dlerror());
}
bool IsDeckLinkAPIPresent_v14_2_1 (void)
{
// If the DeckLink API dynamic library was successfully loaded, return this knowledge to the caller
return gLoadedDeckLinkAPI;
}
IDeckLinkIterator* CreateDeckLinkIteratorInstance_v14_2_1 (void)
{
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
if (gCreateIteratorFunc == NULL)
return NULL;
return gCreateIteratorFunc();
}
IDeckLinkAPIInformation* CreateDeckLinkAPIInformationInstance_v14_2_1 (void)
{
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
if (gCreateAPIInformationFunc == NULL)
return NULL;
return gCreateAPIInformationFunc();
}
IDeckLinkGLScreenPreviewHelper_v14_2_1* CreateOpenGLScreenPreviewHelper_v14_2_1 (void)
{
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
pthread_once(&gPreviewOnceControl, InitDeckLinkPreviewAPI);
if (gCreateOpenGLPreviewFunc == NULL)
return NULL;
return gCreateOpenGLPreviewFunc();
}
IDeckLinkGLScreenPreviewHelper_v14_2_1* CreateOpenGL3ScreenPreviewHelper_v14_2_1 (void)
{
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
pthread_once(&gPreviewOnceControl, InitDeckLinkPreviewAPI);
if (gCreateOpenGL3PreviewFunc == NULL)
return NULL;
return gCreateOpenGL3PreviewFunc();
}
IDeckLinkVideoConversion_v14_2_1* CreateVideoConversionInstance_v14_2_1 (void)
{
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
if (gCreateVideoConversionFunc == NULL)
return NULL;
return gCreateVideoConversionFunc();
}
IDeckLinkDiscovery* CreateDeckLinkDiscoveryInstance_v14_2_1 (void)
{
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
if (gCreateDeckLinkDiscoveryFunc == NULL)
return NULL;
return gCreateDeckLinkDiscoveryFunc();
}
IDeckLinkVideoFrameAncillaryPackets* CreateVideoFrameAncillaryPacketsInstance_v14_2_1 (void)
{
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
if (gCreateVideoFrameAncillaryPacketsFunc == NULL)
return NULL;
return gCreateVideoFrameAncillaryPacketsFunc();
}

View file

@ -0,0 +1,188 @@
/* -LICENSE-START-
** Copyright (c) 2009 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation (the
** "Software") to use, reproduce, display, distribute, sub-license, execute,
** and transmit the Software, and to prepare derivative works of the Software,
** and to permit third-parties to whom the Software is furnished to do so, in
** accordance with:
**
** (1) if the Software is obtained from Blackmagic Design, the End User License
** Agreement for the Software Development Kit ("EULA") available at
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
**
** (2) if the Software is obtained from any third party, such licensing terms
** as notified by that third party,
**
** and all subject to the following:
**
** (3) the copyright notices in the Software and this entire statement,
** including the above license grant, this restriction and the following
** disclaimer, must be included in all copies of the Software, in whole or in
** part, and all derivative works of the Software, unless such copies or
** derivative works are solely in the form of machine-executable object code
** generated by a source language processor.
**
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
**
** A copy of the Software is available free of charge at
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
**
** -LICENSE-END-
**/
#include <stdio.h>
#include <pthread.h>
#include <dlfcn.h>
#include "DeckLinkAPI_v15_2.h"
#define kDeckLinkAPI_Name "libDeckLinkAPI.so"
#define KDeckLinkPreviewAPI_Name "libDeckLinkPreviewAPI.so"
typedef IDeckLinkIterator* (*CreateIteratorFunc)(void);
typedef IDeckLinkAPIInformation* (*CreateAPIInformationFunc)(void);
typedef IDeckLinkGLScreenPreviewHelper* (*CreateOpenGLScreenPreviewHelperFunc)(void);
typedef IDeckLinkGLScreenPreviewHelper* (*CreateOpenGL3ScreenPreviewHelperFunc)(void);
typedef IDeckLinkVideoConversion* (*CreateVideoConversionInstanceFunc)(void);
typedef IDeckLinkDiscovery* (*CreateDeckLinkDiscoveryInstanceFunc)(void);
typedef IDeckLinkVideoFrameAncillaryPackets_v15_2* (*CreateVideoFrameAncillaryPacketsInstanceFunc)(void);
static pthread_once_t gDeckLinkOnceControl = PTHREAD_ONCE_INIT;
static pthread_once_t gPreviewOnceControl = PTHREAD_ONCE_INIT;
static bool gLoadedDeckLinkAPI = false;
static CreateIteratorFunc gCreateIteratorFunc = NULL;
static CreateAPIInformationFunc gCreateAPIInformationFunc = NULL;
static CreateOpenGLScreenPreviewHelperFunc gCreateOpenGLPreviewFunc = NULL;
static CreateOpenGL3ScreenPreviewHelperFunc gCreateOpenGL3PreviewFunc = NULL;
static CreateVideoConversionInstanceFunc gCreateVideoConversionFunc = NULL;
static CreateDeckLinkDiscoveryInstanceFunc gCreateDeckLinkDiscoveryFunc = NULL;
static CreateVideoFrameAncillaryPacketsInstanceFunc gCreateVideoFrameAncillaryPacketsFunc = NULL;
static void InitDeckLinkAPI (void)
{
void *libraryHandle;
libraryHandle = dlopen(kDeckLinkAPI_Name, RTLD_NOW|RTLD_GLOBAL);
if (!libraryHandle)
{
fprintf(stderr, "%s\n", dlerror());
return;
}
gLoadedDeckLinkAPI = true;
gCreateIteratorFunc = (CreateIteratorFunc)dlsym(libraryHandle, "CreateDeckLinkIteratorInstance_0004");
if (!gCreateIteratorFunc)
fprintf(stderr, "%s\n", dlerror());
gCreateAPIInformationFunc = (CreateAPIInformationFunc)dlsym(libraryHandle, "CreateDeckLinkAPIInformationInstance_0001");
if (!gCreateAPIInformationFunc)
fprintf(stderr, "%s\n", dlerror());
gCreateVideoConversionFunc = (CreateVideoConversionInstanceFunc)dlsym(libraryHandle, "CreateVideoConversionInstance_0002");
if (!gCreateVideoConversionFunc)
fprintf(stderr, "%s\n", dlerror());
gCreateDeckLinkDiscoveryFunc = (CreateDeckLinkDiscoveryInstanceFunc)dlsym(libraryHandle, "CreateDeckLinkDiscoveryInstance_0003");
if (!gCreateDeckLinkDiscoveryFunc)
fprintf(stderr, "%s\n", dlerror());
gCreateVideoFrameAncillaryPacketsFunc = (CreateVideoFrameAncillaryPacketsInstanceFunc)dlsym(libraryHandle, "CreateVideoFrameAncillaryPacketsInstance_0001");
if (!gCreateVideoFrameAncillaryPacketsFunc)
fprintf(stderr, "%s\n", dlerror());
}
static void InitDeckLinkPreviewAPI (void)
{
void *libraryHandle;
libraryHandle = dlopen(KDeckLinkPreviewAPI_Name, RTLD_NOW|RTLD_GLOBAL);
if (!libraryHandle)
{
fprintf(stderr, "%s\n", dlerror());
return;
}
gCreateOpenGLPreviewFunc = (CreateOpenGLScreenPreviewHelperFunc)dlsym(libraryHandle, "CreateOpenGLScreenPreviewHelper_0002");
if (!gCreateOpenGLPreviewFunc)
fprintf(stderr, "%s\n", dlerror());
gCreateOpenGL3PreviewFunc = (CreateOpenGL3ScreenPreviewHelperFunc)dlsym(libraryHandle, "CreateOpenGL3ScreenPreviewHelper_0002");
if (!gCreateOpenGL3PreviewFunc)
fprintf(stderr, "%s\n", dlerror());
}
bool IsDeckLinkAPIPresent_v15_2 (void)
{
// If the DeckLink API dynamic library was successfully loaded, return this knowledge to the caller
return gLoadedDeckLinkAPI;
}
IDeckLinkIterator* CreateDeckLinkIteratorInstance (void)
{
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
if (gCreateIteratorFunc == NULL)
return NULL;
return gCreateIteratorFunc();
}
IDeckLinkAPIInformation* CreateDeckLinkAPIInformationInstance (void)
{
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
if (gCreateAPIInformationFunc == NULL)
return NULL;
return gCreateAPIInformationFunc();
}
IDeckLinkGLScreenPreviewHelper* CreateOpenGLScreenPreviewHelper (void)
{
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
pthread_once(&gPreviewOnceControl, InitDeckLinkPreviewAPI);
if (gCreateOpenGLPreviewFunc == NULL)
return NULL;
return gCreateOpenGLPreviewFunc();
}
IDeckLinkGLScreenPreviewHelper* CreateOpenGL3ScreenPreviewHelper (void)
{
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
pthread_once(&gPreviewOnceControl, InitDeckLinkPreviewAPI);
if (gCreateOpenGL3PreviewFunc == NULL)
return NULL;
return gCreateOpenGL3PreviewFunc();
}
IDeckLinkVideoConversion* CreateVideoConversionInstance (void)
{
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
if (gCreateVideoConversionFunc == NULL)
return NULL;
return gCreateVideoConversionFunc();
}
IDeckLinkDiscovery* CreateDeckLinkDiscoveryInstance (void)
{
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
if (gCreateDeckLinkDiscoveryFunc == NULL)
return NULL;
return gCreateDeckLinkDiscoveryFunc();
}
IDeckLinkVideoFrameAncillaryPackets_v15_2* CreateVideoFrameAncillaryPacketsInstance_v15_2 (void)
{
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
if (gCreateVideoFrameAncillaryPacketsFunc == NULL)
return NULL;
return gCreateVideoFrameAncillaryPacketsFunc();
}

View file

@ -0,0 +1,188 @@
/* -LICENSE-START-
** Copyright (c) 2009 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation (the
** "Software") to use, reproduce, display, distribute, sub-license, execute,
** and transmit the Software, and to prepare derivative works of the Software,
** and to permit third-parties to whom the Software is furnished to do so, in
** accordance with:
**
** (1) if the Software is obtained from Blackmagic Design, the End User License
** Agreement for the Software Development Kit ("EULA") available at
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
**
** (2) if the Software is obtained from any third party, such licensing terms
** as notified by that third party,
**
** and all subject to the following:
**
** (3) the copyright notices in the Software and this entire statement,
** including the above license grant, this restriction and the following
** disclaimer, must be included in all copies of the Software, in whole or in
** part, and all derivative works of the Software, unless such copies or
** derivative works are solely in the form of machine-executable object code
** generated by a source language processor.
**
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
**
** A copy of the Software is available free of charge at
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
**
** -LICENSE-END-
**/
#include <stdio.h>
#include <pthread.h>
#include <dlfcn.h>
#include "DeckLinkAPI_v15_3_1.h"
#define kDeckLinkAPI_Name "libDeckLinkAPI.so"
#define KDeckLinkPreviewAPI_Name "libDeckLinkPreviewAPI.so"
typedef IDeckLinkIterator* (*CreateIteratorFunc)(void);
typedef IDeckLinkAPIInformation* (*CreateAPIInformationFunc)(void);
typedef IDeckLinkGLScreenPreviewHelper* (*CreateOpenGLScreenPreviewHelperFunc)(void);
typedef IDeckLinkGLScreenPreviewHelper* (*CreateOpenGL3ScreenPreviewHelperFunc)(void);
typedef IDeckLinkVideoConversion_v15_3_1* (*CreateVideoConversionInstanceFunc)(void);
typedef IDeckLinkDiscovery* (*CreateDeckLinkDiscoveryInstanceFunc)(void);
typedef IDeckLinkVideoFrameAncillaryPackets* (*CreateVideoFrameAncillaryPacketsInstanceFunc)(void);
static pthread_once_t gDeckLinkOnceControl = PTHREAD_ONCE_INIT;
static pthread_once_t gPreviewOnceControl = PTHREAD_ONCE_INIT;
static bool gLoadedDeckLinkAPI = false;
static CreateIteratorFunc gCreateIteratorFunc = NULL;
static CreateAPIInformationFunc gCreateAPIInformationFunc = NULL;
static CreateOpenGLScreenPreviewHelperFunc gCreateOpenGLPreviewFunc = NULL;
static CreateOpenGL3ScreenPreviewHelperFunc gCreateOpenGL3PreviewFunc = NULL;
static CreateVideoConversionInstanceFunc gCreateVideoConversionFunc = NULL;
static CreateDeckLinkDiscoveryInstanceFunc gCreateDeckLinkDiscoveryFunc = NULL;
static CreateVideoFrameAncillaryPacketsInstanceFunc gCreateVideoFrameAncillaryPacketsFunc = NULL;
static void InitDeckLinkAPI (void)
{
void *libraryHandle;
libraryHandle = dlopen(kDeckLinkAPI_Name, RTLD_NOW|RTLD_GLOBAL);
if (!libraryHandle)
{
fprintf(stderr, "%s\n", dlerror());
return;
}
gLoadedDeckLinkAPI = true;
gCreateIteratorFunc = (CreateIteratorFunc)dlsym(libraryHandle, "CreateDeckLinkIteratorInstance_0004");
if (!gCreateIteratorFunc)
fprintf(stderr, "%s\n", dlerror());
gCreateAPIInformationFunc = (CreateAPIInformationFunc)dlsym(libraryHandle, "CreateDeckLinkAPIInformationInstance_0001");
if (!gCreateAPIInformationFunc)
fprintf(stderr, "%s\n", dlerror());
gCreateVideoConversionFunc = (CreateVideoConversionInstanceFunc)dlsym(libraryHandle, "CreateVideoConversionInstance_0002");
if (!gCreateVideoConversionFunc)
fprintf(stderr, "%s\n", dlerror());
gCreateDeckLinkDiscoveryFunc = (CreateDeckLinkDiscoveryInstanceFunc)dlsym(libraryHandle, "CreateDeckLinkDiscoveryInstance_0003");
if (!gCreateDeckLinkDiscoveryFunc)
fprintf(stderr, "%s\n", dlerror());
gCreateVideoFrameAncillaryPacketsFunc = (CreateVideoFrameAncillaryPacketsInstanceFunc)dlsym(libraryHandle, "CreateVideoFrameAncillaryPacketsInstance_0001");
if (!gCreateVideoFrameAncillaryPacketsFunc)
fprintf(stderr, "%s\n", dlerror());
}
static void InitDeckLinkPreviewAPI (void)
{
void *libraryHandle;
libraryHandle = dlopen(KDeckLinkPreviewAPI_Name, RTLD_NOW|RTLD_GLOBAL);
if (!libraryHandle)
{
fprintf(stderr, "%s\n", dlerror());
return;
}
gCreateOpenGLPreviewFunc = (CreateOpenGLScreenPreviewHelperFunc)dlsym(libraryHandle, "CreateOpenGLScreenPreviewHelper_0002");
if (!gCreateOpenGLPreviewFunc)
fprintf(stderr, "%s\n", dlerror());
gCreateOpenGL3PreviewFunc = (CreateOpenGL3ScreenPreviewHelperFunc)dlsym(libraryHandle, "CreateOpenGL3ScreenPreviewHelper_0002");
if (!gCreateOpenGL3PreviewFunc)
fprintf(stderr, "%s\n", dlerror());
}
bool IsDeckLinkAPIPresent (void)
{
// If the DeckLink API dynamic library was successfully loaded, return this knowledge to the caller
return gLoadedDeckLinkAPI;
}
IDeckLinkIterator* CreateDeckLinkIteratorInstance_v15_3_1 (void)
{
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
if (gCreateIteratorFunc == NULL)
return NULL;
return gCreateIteratorFunc();
}
IDeckLinkAPIInformation* CreateDeckLinkAPIInformationInstance_v15_3_1 (void)
{
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
if (gCreateAPIInformationFunc == NULL)
return NULL;
return gCreateAPIInformationFunc();
}
IDeckLinkGLScreenPreviewHelper* CreateOpenGLScreenPreviewHelper_v15_3_1 (void)
{
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
pthread_once(&gPreviewOnceControl, InitDeckLinkPreviewAPI);
if (gCreateOpenGLPreviewFunc == NULL)
return NULL;
return gCreateOpenGLPreviewFunc();
}
IDeckLinkGLScreenPreviewHelper* CreateOpenGL3ScreenPreviewHelper_v15_3_1 (void)
{
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
pthread_once(&gPreviewOnceControl, InitDeckLinkPreviewAPI);
if (gCreateOpenGL3PreviewFunc == NULL)
return NULL;
return gCreateOpenGL3PreviewFunc();
}
IDeckLinkVideoConversion_v15_3_1* CreateVideoConversionInstance_v15_3_1 (void)
{
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
if (gCreateVideoConversionFunc == NULL)
return NULL;
return gCreateVideoConversionFunc();
}
IDeckLinkDiscovery* CreateDeckLinkDiscoveryInstance_v15_3_1 (void)
{
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
if (gCreateDeckLinkDiscoveryFunc == NULL)
return NULL;
return gCreateDeckLinkDiscoveryFunc();
}
IDeckLinkVideoFrameAncillaryPackets* CreateVideoFrameAncillaryPacketsInstance_v15_3_1 (void)
{
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
if (gCreateVideoFrameAncillaryPacketsFunc == NULL)
return NULL;
return gCreateVideoFrameAncillaryPacketsFunc();
}

View file

@ -0,0 +1,68 @@
/* -LICENSE-START-
** Copyright (c) 2022 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation (the
** "Software") to use, reproduce, display, distribute, sub-license, execute,
** and transmit the Software, and to prepare derivative works of the Software,
** and to permit third-parties to whom the Software is furnished to do so, in
** accordance with:
**
** (1) if the Software is obtained from Blackmagic Design, the End User License
** Agreement for the Software Development Kit (EULA) available at
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
**
** (2) if the Software is obtained from any third party, such licensing terms
** as notified by that third party,
**
** and all subject to the following:
**
** (3) the copyright notices in the Software and this entire statement,
** including the above license grant, this restriction and the following
** disclaimer, must be included in all copies of the Software, in whole or in
** part, and all derivative works of the Software, unless such copies or
** derivative works are solely in the form of machine-executable object code
** generated by a source language processor.
**
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
**
** A copy of the Software is available free of charge at
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
**
** -LICENSE-END-
*/
#ifndef BMD_DECKLINKAPIGLSCREENPREVIEW_v14_2_1_H
#define BMD_DECKLINKAPIGLSCREENPREVIEW_v14_2_1_H
#include "DeckLinkAPI.h"
#include "DeckLinkAPIVideoFrame_v14_2_1.h"
// Type Declarations
BMD_CONST REFIID IID_IDeckLinkGLScreenPreviewHelper_v14_2_1 = /* 504E2209-CAC7-4C1A-9FB4-C5BB6274D22F */ { 0x50, 0x4E, 0x22, 0x09, 0xCA, 0xC7, 0x4C, 0x1A, 0x9F, 0xB4, 0xC5, 0xBB, 0x62, 0x74, 0xD2, 0x2F };
/* Interface IDeckLinkGLScreenPreviewHelper - Created with CoCreateInstance on platforms with native COM support or from CreateOpenGLScreenPreviewHelper/CreateOpenGL3ScreenPreviewHelper on other platforms. */
class BMD_PUBLIC IDeckLinkGLScreenPreviewHelper_v14_2_1 : public IUnknown
{
public:
/* Methods must be called with OpenGL context set */
virtual HRESULT InitializeGL (void) = 0;
virtual HRESULT PaintGL (void) = 0;
virtual HRESULT SetFrame (/* in */ IDeckLinkVideoFrame_v14_2_1* theFrame) = 0;
virtual HRESULT Set3DPreviewFormat (/* in */ BMD3DPreviewFormat previewFormat) = 0;
protected:
virtual ~IDeckLinkGLScreenPreviewHelper_v14_2_1 () {} // call Release method to drop reference count
};
#endif

View file

@ -0,0 +1,61 @@
/* -LICENSE-START-
** Copyright (c) 2022 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation (the
** "Software") to use, reproduce, display, distribute, sub-license, execute,
** and transmit the Software, and to prepare derivative works of the Software,
** and to permit third-parties to whom the Software is furnished to do so, in
** accordance with:
**
** (1) if the Software is obtained from Blackmagic Design, the End User License
** Agreement for the Software Development Kit (EULA) available at
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
**
** (2) if the Software is obtained from any third party, such licensing terms
** as notified by that third party,
**
** and all subject to the following:
**
** (3) the copyright notices in the Software and this entire statement,
** including the above license grant, this restriction and the following
** disclaimer, must be included in all copies of the Software, in whole or in
** part, and all derivative works of the Software, unless such copies or
** derivative works are solely in the form of machine-executable object code
** generated by a source language processor.
**
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
**
** A copy of the Software is available free of charge at
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
**
** -LICENSE-END-
*/
#ifndef BMD_DECKLINKAPIMEMORYALLOCATOR_v14_2_1_H
#define BMD_DECKLINKAPIMEMORYALLOCATOR_v14_2_1_H
#include "DeckLinkAPI.h"
// Type Declarations
BMD_CONST REFIID IID_IDeckLinkMemoryAllocator_v14_2_1 = /* B36EB6E7-9D29-4AA8-92EF-843B87A289E8 */ { 0xB3, 0x6E, 0xB6, 0xE7, 0x9D, 0x29, 0x4A, 0xA8, 0x92, 0xEF, 0x84, 0x3B, 0x87, 0xA2, 0x89, 0xE8 };
/* Interface IDeckLinkMemoryAllocator_v14_2_1 - Created with CoCreateInstance. */
class BMD_PUBLIC IDeckLinkMemoryAllocator_v14_2_1 : public IUnknown
{
public:
virtual HRESULT AllocateBuffer (/* in */ uint32_t bufferSize, /* out */ void** allocatedBuffer) = 0;
virtual HRESULT ReleaseBuffer (/* in */ void* buffer) = 0;
virtual HRESULT Commit (void) = 0;
virtual HRESULT Decommit (void) = 0;
};
#endif

View file

@ -0,0 +1,65 @@
/* -LICENSE-START-
** Copyright (c) 2022 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation (the
** "Software") to use, reproduce, display, distribute, sub-license, execute,
** and transmit the Software, and to prepare derivative works of the Software,
** and to permit third-parties to whom the Software is furnished to do so, in
** accordance with:
**
** (1) if the Software is obtained from Blackmagic Design, the End User License
** Agreement for the Software Development Kit (EULA) available at
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
**
** (2) if the Software is obtained from any third party, such licensing terms
** as notified by that third party,
**
** and all subject to the following:
**
** (3) the copyright notices in the Software and this entire statement,
** including the above license grant, this restriction and the following
** disclaimer, must be included in all copies of the Software, in whole or in
** part, and all derivative works of the Software, unless such copies or
** derivative works are solely in the form of machine-executable object code
** generated by a source language processor.
**
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
**
** A copy of the Software is available free of charge at
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
**
** -LICENSE-END-
*/
#ifndef BMD_DECKLINKAPIMETALSCREENPREVIEW_v14_2_1_H
#define BMD_DECKLINKAPIMETALSCREENPREVIEW_v14_2_1_H
#include "DeckLinkAPI.h"
#include "DeckLinkAPIVideoFrame_v14_2_1.h"
// Type Declarations
BMD_CONST REFIID IID_IDeckLinkMetalScreenPreviewHelper_v14_2_1 = /* 1AB252C5-DACB-4AE8-A58B-5320DE9CE373 */ { 0x1A, 0xB2, 0x52, 0xC5, 0xDA, 0xCB, 0x4A, 0xE8, 0xA5, 0x8B, 0x53, 0x20, 0xDE, 0x9C, 0xE3, 0x73 };
/* Interface IDeckLinkMetalScreenPreviewHelper - Created with CreateMetalScreenPreviewHelper(). */
class BMD_PUBLIC IDeckLinkMetalScreenPreviewHelper_v14_2_1 : public IUnknown
{
public:
virtual HRESULT Initialize (/* in */ void* device) = 0;
virtual HRESULT Draw (/* in */ void* cmdBuffer, /* in */ void* renderPassDescriptor, /* in */ void* viewport) = 0;
virtual HRESULT SetFrame (/* in */ IDeckLinkVideoFrame_v14_2_1* theFrame) = 0;
virtual HRESULT Set3DPreviewFormat (/* in */ BMD3DPreviewFormat previewFormat) = 0;
protected:
virtual ~IDeckLinkMetalScreenPreviewHelper_v14_2_1 () {} // call Release method to drop reference count
};
#endif

View file

@ -0,0 +1,291 @@
/* -LICENSE-START-
** Copyright (c) 2026 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation covered by
** this license (the "Software") to use, reproduce, display, distribute,
** execute, and transmit the Software, and to prepare derivative works of the
** Software, and to permit third-parties to whom the Software is furnished to
** do so, all subject to the following:
**
** The copyright notices in the Software and this entire statement, including
** the above license grant, this restriction and the following disclaimer,
** must be included in all copies of the Software, in whole or in part, and
** all derivative works of the Software, unless such copies or derivative
** works are solely in the form of machine-executable object code generated by
** a source language processor.
**
** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
** -LICENSE-END-
*/
/*
* -- AUTOMATICALLY GENERATED - DO NOT EDIT ---
*/
#ifndef BMD_DECKLINKAPIMODES_H
#define BMD_DECKLINKAPIMODES_H
#ifndef BMD_CONST
#if defined(_MSC_VER)
#define BMD_CONST __declspec(selectany) static const
#else
#define BMD_CONST static const
#endif
#endif
#ifndef BMD_PUBLIC
#define BMD_PUBLIC
#endif
// Type Declarations
// Interface ID Declarations
BMD_CONST REFIID IID_IDeckLinkDisplayModeIterator = /* 9C88499F-F601-4021-B80B-032E4EB41C35 */ { 0x9C,0x88,0x49,0x9F,0xF6,0x01,0x40,0x21,0xB8,0x0B,0x03,0x2E,0x4E,0xB4,0x1C,0x35 };
BMD_CONST REFIID IID_IDeckLinkDisplayMode = /* 3EB2C1AB-0A3D-4523-A3AD-F40D7FB14E78 */ { 0x3E,0xB2,0xC1,0xAB,0x0A,0x3D,0x45,0x23,0xA3,0xAD,0xF4,0x0D,0x7F,0xB1,0x4E,0x78 };
/* Enum BMDDisplayMode - BMDDisplayMode enumerates the video modes supported. */
typedef uint32_t BMDDisplayMode;
enum _BMDDisplayMode {
/* SD Modes */
bmdModeNTSC = /* 'ntsc' */ 0x6E747363,
bmdModeNTSC2398 = /* 'nt23' */ 0x6E743233, // 3:2 pulldown
bmdModePAL = /* 'pal ' */ 0x70616C20,
bmdModeNTSCp = /* 'ntsp' */ 0x6E747370,
bmdModePALp = /* 'palp' */ 0x70616C70,
/* HD 1080 Modes */
bmdModeHD1080p2398 = /* '23ps' */ 0x32337073,
bmdModeHD1080p24 = /* '24ps' */ 0x32347073,
bmdModeHD1080p25 = /* 'Hp25' */ 0x48703235,
bmdModeHD1080p2997 = /* 'Hp29' */ 0x48703239,
bmdModeHD1080p30 = /* 'Hp30' */ 0x48703330,
bmdModeHD1080p4795 = /* 'Hp47' */ 0x48703437,
bmdModeHD1080p48 = /* 'Hp48' */ 0x48703438,
bmdModeHD1080p50 = /* 'Hp50' */ 0x48703530,
bmdModeHD1080p5994 = /* 'Hp59' */ 0x48703539,
bmdModeHD1080p6000 = /* 'Hp60' */ 0x48703630, // N.B. This _really_ is 60.00 Hz.
bmdModeHD1080p9590 = /* 'Hp95' */ 0x48703935,
bmdModeHD1080p96 = /* 'Hp96' */ 0x48703936,
bmdModeHD1080p100 = /* 'Hp10' */ 0x48703130,
bmdModeHD1080p11988 = /* 'Hp11' */ 0x48703131,
bmdModeHD1080p120 = /* 'Hp12' */ 0x48703132,
bmdModeHD1080i50 = /* 'Hi50' */ 0x48693530,
bmdModeHD1080i5994 = /* 'Hi59' */ 0x48693539,
bmdModeHD1080i6000 = /* 'Hi60' */ 0x48693630, // N.B. This _really_ is 60.00 Hz.
/* HD 720 Modes */
bmdModeHD720p50 = /* 'hp50' */ 0x68703530,
bmdModeHD720p5994 = /* 'hp59' */ 0x68703539,
bmdModeHD720p60 = /* 'hp60' */ 0x68703630,
/* 2K Modes */
bmdMode2k2398 = /* '2k23' */ 0x326B3233,
bmdMode2k24 = /* '2k24' */ 0x326B3234,
bmdMode2k25 = /* '2k25' */ 0x326B3235,
/* 2K DCI Modes */
bmdMode2kDCI2398 = /* '2d23' */ 0x32643233,
bmdMode2kDCI24 = /* '2d24' */ 0x32643234,
bmdMode2kDCI25 = /* '2d25' */ 0x32643235,
bmdMode2kDCI2997 = /* '2d29' */ 0x32643239,
bmdMode2kDCI30 = /* '2d30' */ 0x32643330,
bmdMode2kDCI4795 = /* '2d47' */ 0x32643437,
bmdMode2kDCI48 = /* '2d48' */ 0x32643438,
bmdMode2kDCI50 = /* '2d50' */ 0x32643530,
bmdMode2kDCI5994 = /* '2d59' */ 0x32643539,
bmdMode2kDCI60 = /* '2d60' */ 0x32643630,
bmdMode2kDCI9590 = /* '2d95' */ 0x32643935,
bmdMode2kDCI96 = /* '2d96' */ 0x32643936,
bmdMode2kDCI100 = /* '2d10' */ 0x32643130,
bmdMode2kDCI11988 = /* '2d11' */ 0x32643131,
bmdMode2kDCI120 = /* '2d12' */ 0x32643132,
/* 4K UHD Modes */
bmdMode4K2160p2398 = /* '4k23' */ 0x346B3233,
bmdMode4K2160p24 = /* '4k24' */ 0x346B3234,
bmdMode4K2160p25 = /* '4k25' */ 0x346B3235,
bmdMode4K2160p2997 = /* '4k29' */ 0x346B3239,
bmdMode4K2160p30 = /* '4k30' */ 0x346B3330,
bmdMode4K2160p4795 = /* '4k47' */ 0x346B3437,
bmdMode4K2160p48 = /* '4k48' */ 0x346B3438,
bmdMode4K2160p50 = /* '4k50' */ 0x346B3530,
bmdMode4K2160p5994 = /* '4k59' */ 0x346B3539,
bmdMode4K2160p60 = /* '4k60' */ 0x346B3630,
bmdMode4K2160p9590 = /* '4k95' */ 0x346B3935,
bmdMode4K2160p96 = /* '4k96' */ 0x346B3936,
bmdMode4K2160p100 = /* '4k10' */ 0x346B3130,
bmdMode4K2160p11988 = /* '4k11' */ 0x346B3131,
bmdMode4K2160p120 = /* '4k12' */ 0x346B3132,
/* 4K DCI Modes */
bmdMode4kDCI2398 = /* '4d23' */ 0x34643233,
bmdMode4kDCI24 = /* '4d24' */ 0x34643234,
bmdMode4kDCI25 = /* '4d25' */ 0x34643235,
bmdMode4kDCI2997 = /* '4d29' */ 0x34643239,
bmdMode4kDCI30 = /* '4d30' */ 0x34643330,
bmdMode4kDCI4795 = /* '4d47' */ 0x34643437,
bmdMode4kDCI48 = /* '4d48' */ 0x34643438,
bmdMode4kDCI50 = /* '4d50' */ 0x34643530,
bmdMode4kDCI5994 = /* '4d59' */ 0x34643539,
bmdMode4kDCI60 = /* '4d60' */ 0x34643630,
bmdMode4kDCI9590 = /* '4d95' */ 0x34643935,
bmdMode4kDCI96 = /* '4d96' */ 0x34643936,
bmdMode4kDCI100 = /* '4d10' */ 0x34643130,
bmdMode4kDCI11988 = /* '4d11' */ 0x34643131,
bmdMode4kDCI120 = /* '4d12' */ 0x34643132,
/* 8K UHD Modes */
bmdMode8K4320p2398 = /* '8k23' */ 0x386B3233,
bmdMode8K4320p24 = /* '8k24' */ 0x386B3234,
bmdMode8K4320p25 = /* '8k25' */ 0x386B3235,
bmdMode8K4320p2997 = /* '8k29' */ 0x386B3239,
bmdMode8K4320p30 = /* '8k30' */ 0x386B3330,
bmdMode8K4320p4795 = /* '8k47' */ 0x386B3437,
bmdMode8K4320p48 = /* '8k48' */ 0x386B3438,
bmdMode8K4320p50 = /* '8k50' */ 0x386B3530,
bmdMode8K4320p5994 = /* '8k59' */ 0x386B3539,
bmdMode8K4320p60 = /* '8k60' */ 0x386B3630,
/* 8K DCI Modes */
bmdMode8kDCI2398 = /* '8d23' */ 0x38643233,
bmdMode8kDCI24 = /* '8d24' */ 0x38643234,
bmdMode8kDCI25 = /* '8d25' */ 0x38643235,
bmdMode8kDCI2997 = /* '8d29' */ 0x38643239,
bmdMode8kDCI30 = /* '8d30' */ 0x38643330,
bmdMode8kDCI4795 = /* '8d47' */ 0x38643437,
bmdMode8kDCI48 = /* '8d48' */ 0x38643438,
bmdMode8kDCI50 = /* '8d50' */ 0x38643530,
bmdMode8kDCI5994 = /* '8d59' */ 0x38643539,
bmdMode8kDCI60 = /* '8d60' */ 0x38643630,
/* PC Modes */
bmdMode640x480p60 = /* 'vga6' */ 0x76676136,
bmdMode800x600p60 = /* 'svg6' */ 0x73766736,
bmdMode1440x900p50 = /* 'wxg5' */ 0x77786735,
bmdMode1440x900p60 = /* 'wxg6' */ 0x77786736,
bmdMode1440x1080p50 = /* 'sxg5' */ 0x73786735,
bmdMode1440x1080p60 = /* 'sxg6' */ 0x73786736,
bmdMode1600x1200p50 = /* 'uxg5' */ 0x75786735,
bmdMode1600x1200p60 = /* 'uxg6' */ 0x75786736,
bmdMode1920x1200p50 = /* 'wux5' */ 0x77757835,
bmdMode1920x1200p60 = /* 'wux6' */ 0x77757836,
bmdMode1920x1440p50 = /* '1945' */ 0x31393435,
bmdMode1920x1440p60 = /* '1946' */ 0x31393436,
bmdMode2560x1440p50 = /* 'wqh5' */ 0x77716835,
bmdMode2560x1440p60 = /* 'wqh6' */ 0x77716836,
bmdMode2560x1600p50 = /* 'wqx5' */ 0x77717835,
bmdMode2560x1600p60 = /* 'wqx6' */ 0x77717836,
bmdModeUnknown = /* 'iunk' */ 0x69756E6B
};
/* Enum BMDFieldDominance - BMDFieldDominance enumerates settings applicable to video fields. */
typedef uint32_t BMDFieldDominance;
enum _BMDFieldDominance {
bmdUnknownFieldDominance = 0,
bmdLowerFieldFirst = /* 'lowr' */ 0x6C6F7772,
bmdUpperFieldFirst = /* 'uppr' */ 0x75707072,
bmdProgressiveFrame = /* 'prog' */ 0x70726F67,
bmdProgressiveSegmentedFrame = /* 'psf ' */ 0x70736620
};
/* Enum BMDPixelFormat - Video pixel formats supported for output/input */
typedef uint32_t BMDPixelFormat;
enum _BMDPixelFormat {
bmdFormatUnspecified = 0,
bmdFormat8BitYUV = /* '2vuy' */ 0x32767579,
bmdFormat10BitYUV = /* 'v210' */ 0x76323130,
bmdFormat10BitYUVA = /* 'Ay10' */ 0x41793130, // Big-endian YUVA 10 bit per component with SMPTE video levels (64-940) for YUV but full range alpha
bmdFormat8BitARGB = 32,
bmdFormat8BitBGRA = /* 'BGRA' */ 0x42475241,
bmdFormat10BitRGB = /* 'r210' */ 0x72323130, // Big-endian RGB 10-bit per component with SMPTE video levels (64-940). Packed as 2:10:10:10
bmdFormat12BitRGB = /* 'R12B' */ 0x52313242, // Big-endian RGB 12-bit per component with full range (0-4095). Packed as 12-bit per component
bmdFormat12BitRGBLE = /* 'R12L' */ 0x5231324C, // Little-endian RGB 12-bit per component with full range (0-4095). Packed as 12-bit per component
bmdFormat10BitRGBXLE = /* 'R10l' */ 0x5231306C, // Little-endian 10-bit RGB with SMPTE video levels (64-940)
bmdFormat10BitRGBX = /* 'R10b' */ 0x52313062, // Big-endian 10-bit RGB with SMPTE video levels (64-940)
/* Formats supported only by devices that can be queried for an IDeckLinkEncoderInput */
bmdFormatH265 = /* 'hev1' */ 0x68657631,
bmdFormatDNxHR = /* 'AVdh' */ 0x41566468
};
/* Enum BMDDisplayModeFlags - Flags to describe the characteristics of an IDeckLinkDisplayMode. */
typedef uint32_t BMDDisplayModeFlags;
enum _BMDDisplayModeFlags {
bmdDisplayModeSupports3D = 1 << 0,
bmdDisplayModeColorspaceRec601 = 1 << 1,
bmdDisplayModeColorspaceRec709 = 1 << 2,
bmdDisplayModeColorspaceRec2020 = 1 << 3
};
#if defined(__cplusplus)
// Forward Declarations
class IDeckLinkDisplayModeIterator;
class IDeckLinkDisplayMode;
/* Interface IDeckLinkDisplayModeIterator - Enumerates over supported input/output display modes. */
class BMD_PUBLIC IDeckLinkDisplayModeIterator : public IUnknown
{
public:
virtual HRESULT Next (/* out */ IDeckLinkDisplayMode** deckLinkDisplayMode) = 0;
protected:
virtual ~IDeckLinkDisplayModeIterator () {} // call Release method to drop reference count
};
/* Interface IDeckLinkDisplayMode - Represents a display mode */
class BMD_PUBLIC IDeckLinkDisplayMode : public IUnknown
{
public:
virtual HRESULT GetName (/* out */ const char** name) = 0;
virtual BMDDisplayMode GetDisplayMode (void) = 0;
virtual long GetWidth (void) = 0;
virtual long GetHeight (void) = 0;
virtual HRESULT GetFrameRate (/* out */ BMDTimeValue* frameDuration, /* out */ BMDTimeScale* timeScale) = 0;
virtual BMDFieldDominance GetFieldDominance (void) = 0;
virtual BMDDisplayModeFlags GetFlags (void) = 0;
protected:
virtual ~IDeckLinkDisplayMode () {} // call Release method to drop reference count
};
/* Functions */
extern "C" {
}
#endif /* defined(__cplusplus) */
#endif /* defined(BMD_DECKLINKAPIMODES_H) */

View file

@ -0,0 +1,62 @@
/* -LICENSE-START-
** Copyright (c) 2022 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation (the
** "Software") to use, reproduce, display, distribute, sub-license, execute,
** and transmit the Software, and to prepare derivative works of the Software,
** and to permit third-parties to whom the Software is furnished to do so, in
** accordance with:
**
** (1) if the Software is obtained from Blackmagic Design, the End User License
** Agreement for the Software Development Kit (EULA) available at
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
**
** (2) if the Software is obtained from any third party, such licensing terms
** as notified by that third party,
**
** and all subject to the following:
**
** (3) the copyright notices in the Software and this entire statement,
** including the above license grant, this restriction and the following
** disclaimer, must be included in all copies of the Software, in whole or in
** part, and all derivative works of the Software, unless such copies or
** derivative works are solely in the form of machine-executable object code
** generated by a source language processor.
**
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
**
** A copy of the Software is available free of charge at
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
**
** -LICENSE-END-
*/
#ifndef BMD_DECKLINKAPISCREENPREVIEWCALLBACK_v14_2_1_H
#define BMD_DECKLINKAPISCREENPREVIEWCALLBACK_v14_2_1_H
#include "DeckLinkAPI.h"
#include "DeckLinkAPIVideoFrame_v14_2_1.h"
// Type Declarations
BMD_CONST REFIID IID_IDeckLinkScreenPreviewCallback_v14_2_1 = /* B1D3F49A-85FE-4C5D-95C8-0B5D5DCCD438 */ { 0xB1, 0xD3, 0xF4, 0x9A, 0x85, 0xFE, 0x4C, 0x5D, 0x95, 0xC8, 0x0B, 0x5D, 0x5D, 0xCC, 0xD4, 0x38 };
/* Interface IDeckLinkScreenPreviewCallback_v14_2_1 - Screen preview callback */
class BMD_PUBLIC IDeckLinkScreenPreviewCallback_v14_2_1 : public IUnknown
{
public:
virtual HRESULT DrawFrame (/* in */ IDeckLinkVideoFrame_v14_2_1* theFrame) = 0;
protected:
virtual ~IDeckLinkScreenPreviewCallback_v14_2_1 () {} // call Release method to drop reference count
};
#endif /* defined(BMD_DECKLINKAPISCREENPREVIEWCALLBACK_v14_2_1_H) */

View file

@ -0,0 +1,140 @@
/* -LICENSE-START-
** Copyright (c) 2026 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation covered by
** this license (the "Software") to use, reproduce, display, distribute,
** execute, and transmit the Software, and to prepare derivative works of the
** Software, and to permit third-parties to whom the Software is furnished to
** do so, all subject to the following:
**
** The copyright notices in the Software and this entire statement, including
** the above license grant, this restriction and the following disclaimer,
** must be included in all copies of the Software, in whole or in part, and
** all derivative works of the Software, unless such copies or derivative
** works are solely in the form of machine-executable object code generated by
** a source language processor.
**
** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
** -LICENSE-END-
*/
/*
* -- AUTOMATICALLY GENERATED - DO NOT EDIT ---
*/
#ifndef BMD_DECKLINKAPITYPES_H
#define BMD_DECKLINKAPITYPES_H
#ifndef BMD_CONST
#if defined(_MSC_VER)
#define BMD_CONST __declspec(selectany) static const
#else
#define BMD_CONST static const
#endif
#endif
#ifndef BMD_PUBLIC
#define BMD_PUBLIC
#endif
// Type Declarations
typedef int64_t BMDTimeValue;
typedef int64_t BMDTimeScale;
typedef uint32_t BMDTimecodeBCD;
typedef uint32_t BMDTimecodeUserBits;
typedef int64_t BMDIPFlowID;
// Interface ID Declarations
BMD_CONST REFIID IID_IDeckLinkTimecode = /* BC6CFBD3-8317-4325-AC1C-1216391E9340 */ { 0xBC,0x6C,0xFB,0xD3,0x83,0x17,0x43,0x25,0xAC,0x1C,0x12,0x16,0x39,0x1E,0x93,0x40 };
/* Enum BMDTimecodeFlags - Timecode flags */
typedef uint32_t BMDTimecodeFlags;
enum _BMDTimecodeFlags {
bmdTimecodeFlagDefault = 0,
bmdTimecodeIsDropFrame = 1 << 0,
bmdTimecodeFieldMark = 1 << 1,
bmdTimecodeColorFrame = 1 << 2,
bmdTimecodeEmbedRecordingTrigger = 1 << 3, // On SDI recording trigger utilises a user-bit.
bmdTimecodeRecordingTriggered = 1 << 4
};
/* Enum BMDVideoConnection - Video connection types */
typedef uint32_t BMDVideoConnection;
enum _BMDVideoConnection {
bmdVideoConnectionUnspecified = 0,
bmdVideoConnectionSDI = 1 << 0,
bmdVideoConnectionHDMI = 1 << 1,
bmdVideoConnectionOpticalSDI = 1 << 2,
bmdVideoConnectionComponent = 1 << 3,
bmdVideoConnectionComposite = 1 << 4,
bmdVideoConnectionSVideo = 1 << 5,
bmdVideoConnectionEthernet = 1 << 6,
bmdVideoConnectionOpticalEthernet = 1 << 7,
bmdVideoConnectionInternal = 1 << 8
};
/* Enum BMDAudioConnection - Audio connection types */
typedef uint32_t BMDAudioConnection;
enum _BMDAudioConnection {
bmdAudioConnectionEmbedded = 1 << 0,
bmdAudioConnectionAESEBU = 1 << 1,
bmdAudioConnectionAnalog = 1 << 2,
bmdAudioConnectionAnalogXLR = 1 << 3,
bmdAudioConnectionAnalogRCA = 1 << 4,
bmdAudioConnectionMicrophone = 1 << 5,
bmdAudioConnectionHeadphones = 1 << 6
};
/* Enum BMDDeckControlConnection - Deck control connections */
typedef uint32_t BMDDeckControlConnection;
enum _BMDDeckControlConnection {
bmdDeckControlConnectionRS422Remote1 = 1 << 0,
bmdDeckControlConnectionRS422Remote2 = 1 << 1
};
#if defined(__cplusplus)
// Forward Declarations
class IDeckLinkTimecode;
/* Interface IDeckLinkTimecode - Used for video frame timecode representation. */
class BMD_PUBLIC IDeckLinkTimecode : public IUnknown
{
public:
virtual BMDTimecodeBCD GetBCD (void) = 0;
virtual HRESULT GetComponents (/* out */ uint8_t* hours, /* out */ uint8_t* minutes, /* out */ uint8_t* seconds, /* out */ uint8_t* frames) = 0;
virtual HRESULT GetString (/* out */ const char** timecode) = 0;
virtual BMDTimecodeFlags GetFlags (void) = 0;
virtual HRESULT GetTimecodeUserBits (/* out */ BMDTimecodeUserBits* userBits) = 0;
protected:
virtual ~IDeckLinkTimecode () {} // call Release method to drop reference count
};
/* Functions */
extern "C" {
}
#endif /* defined(__cplusplus) */
#endif /* defined(BMD_DECKLINKAPITYPES_H) */

View file

@ -0,0 +1,50 @@
/* -LICENSE-START-
* ** Copyright (c) 2014 Blackmagic Design
* **
* ** Permission is hereby granted, free of charge, to any person or organization
* ** obtaining a copy of the software and accompanying documentation (the
* ** "Software") to use, reproduce, display, distribute, sub-license, execute,
* ** and transmit the Software, and to prepare derivative works of the Software,
* ** and to permit third-parties to whom the Software is furnished to do so, in
* ** accordance with:
* **
* ** (1) if the Software is obtained from Blackmagic Design, the End User License
* ** Agreement for the Software Development Kit ("EULA") available at
* ** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
* **
* ** (2) if the Software is obtained from any third party, such licensing terms
* ** as notified by that third party,
* **
* ** and all subject to the following:
* **
* ** (3) the copyright notices in the Software and this entire statement,
* ** including the above license grant, this restriction and the following
* ** disclaimer, must be included in all copies of the Software, in whole or in
* ** part, and all derivative works of the Software, unless such copies or
* ** derivative works are solely in the form of machine-executable object code
* ** generated by a source language processor.
* **
* ** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* ** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* ** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
* ** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
* ** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
* ** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* ** DEALINGS IN THE SOFTWARE.
* **
* ** A copy of the Software is available free of charge at
* ** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
* **
* ** -LICENSE-END-
* */
/* DeckLinkAPIVersion.h */
#ifndef __DeckLink_API_Version_h__
#define __DeckLink_API_Version_h__
#define BLACKMAGIC_DECKLINK_API_VERSION 0x10000000
#define BLACKMAGIC_DECKLINK_API_VERSION_STRING "16.0"
#endif // __DeckLink_API_Version_h__

View file

@ -0,0 +1,62 @@
/* -LICENSE-START-
** Copyright (c) 2022 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation (the
** "Software") to use, reproduce, display, distribute, sub-license, execute,
** and transmit the Software, and to prepare derivative works of the Software,
** and to permit third-parties to whom the Software is furnished to do so, in
** accordance with:
**
** (1) if the Software is obtained from Blackmagic Design, the End User License
** Agreement for the Software Development Kit (EULA) available at
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
**
** (2) if the Software is obtained from any third party, such licensing terms
** as notified by that third party,
**
** and all subject to the following:
**
** (3) the copyright notices in the Software and this entire statement,
** including the above license grant, this restriction and the following
** disclaimer, must be included in all copies of the Software, in whole or in
** part, and all derivative works of the Software, unless such copies or
** derivative works are solely in the form of machine-executable object code
** generated by a source language processor.
**
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
**
** A copy of the Software is available free of charge at
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
**
** -LICENSE-END-
*/
#ifndef BMD_DECKLINKAPIVIDEOCONVERSION_v14_2_1_H
#define BMD_DECKLINKAPIVIDEOCONVERSION_v14_2_1_H
#include "DeckLinkAPI.h"
#include "DeckLinkAPIVideoFrame_v14_2_1.h"
// Type Declarations
BMD_CONST REFIID IID_IDeckLinkVideoConversion_v14_2_1 = /* 3BBCB8A2-DA2C-42D9-B5D8-88083644E99A */ { 0x3B, 0xBC, 0xB8, 0xA2, 0xDA, 0x2C, 0x42, 0xD9, 0xB5, 0xD8, 0x88, 0x08, 0x36, 0x44, 0xE9, 0x9A };
/* Interface IDeckLinkVideoConversion - Created with CoCreateInstance. */
class BMD_PUBLIC IDeckLinkVideoConversion_v14_2_1 : public IUnknown
{
public:
virtual HRESULT ConvertFrame (/* in */ IDeckLinkVideoFrame_v14_2_1* srcFrame, /* in */ IDeckLinkVideoFrame_v14_2_1* dstFrame) = 0;
protected:
virtual ~IDeckLinkVideoConversion_v14_2_1 () {} // call Release method to drop reference count
};
#endif

View file

@ -0,0 +1,88 @@
/* -LICENSE-START-
** Copyright (c) 2017 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation (the
** "Software") to use, reproduce, display, distribute, sub-license, execute,
** and transmit the Software, and to prepare derivative works of the Software,
** and to permit third-parties to whom the Software is furnished to do so, in
** accordance with:
**
** (1) if the Software is obtained from Blackmagic Design, the End User License
** Agreement for the Software Development Kit ("EULA") available at
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
**
** (2) if the Software is obtained from any third party, such licensing terms
** as notified by that third party,
**
** and all subject to the following:
**
** (3) the copyright notices in the Software and this entire statement,
** including the above license grant, this restriction and the following
** disclaimer, must be included in all copies of the Software, in whole or in
** part, and all derivative works of the Software, unless such copies or
** derivative works are solely in the form of machine-executable object code
** generated by a source language processor.
**
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
**
** A copy of the Software is available free of charge at
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
**
** -LICENSE-END-
*/
#ifndef BMD_DECKLINKAPIVIDEOENCODERINPUT_v10_11_H
#define BMD_DECKLINKAPIVIDEOENCODERINPUT_v10_11_H
#include "DeckLinkAPI.h"
#include "DeckLinkAPI_v10_11.h"
#include "DeckLinkAPIMemoryAllocator_v14_2_1.h"
// Type Declarations
BMD_CONST REFIID IID_IDeckLinkEncoderInput_v10_11 = /* 270587DA-6B7D-42E7-A1F0-6D853F581185 */ {0x27,0x05,0x87,0xDA,0x6B,0x7D,0x42,0xE7,0xA1,0xF0,0x6D,0x85,0x3F,0x58,0x11,0x85};
/* Interface IDeckLinkEncoderInput_v10_11 - Created by QueryInterface from IDeckLink. */
class IDeckLinkEncoderInput_v10_11 : public IUnknown
{
public:
virtual HRESULT DoesSupportVideoMode (/* in */ BMDDisplayMode displayMode, /* in */ BMDPixelFormat pixelFormat, /* in */ BMDVideoInputFlags flags, /* out */ BMDDisplayModeSupport_v10_11 *result, /* out */ IDeckLinkDisplayMode **resultDisplayMode) = 0;
virtual HRESULT GetDisplayModeIterator (/* out */ IDeckLinkDisplayModeIterator **iterator) = 0;
/* Video Input */
virtual HRESULT EnableVideoInput (/* in */ BMDDisplayMode displayMode, /* in */ BMDPixelFormat pixelFormat, /* in */ BMDVideoInputFlags flags) = 0;
virtual HRESULT DisableVideoInput (void) = 0;
virtual HRESULT GetAvailablePacketsCount (/* out */ uint32_t *availablePacketsCount) = 0;
virtual HRESULT SetMemoryAllocator (/* in */ IDeckLinkMemoryAllocator_v14_2_1 *theAllocator) = 0;
/* Audio Input */
virtual HRESULT EnableAudioInput (/* in */ BMDAudioFormat audioFormat, /* in */ BMDAudioSampleRate sampleRate, /* in */ BMDAudioSampleType sampleType, /* in */ uint32_t channelCount) = 0;
virtual HRESULT DisableAudioInput (void) = 0;
virtual HRESULT GetAvailableAudioSampleFrameCount (/* out */ uint32_t *availableSampleFrameCount) = 0;
/* Input Control */
virtual HRESULT StartStreams (void) = 0;
virtual HRESULT StopStreams (void) = 0;
virtual HRESULT PauseStreams (void) = 0;
virtual HRESULT FlushStreams (void) = 0;
virtual HRESULT SetCallback (/* in */ IDeckLinkEncoderInputCallback *theCallback) = 0;
/* Hardware Timing */
virtual HRESULT GetHardwareReferenceClock (/* in */ BMDTimeScale desiredTimeScale, /* out */ BMDTimeValue *hardwareTime, /* out */ BMDTimeValue *timeInFrame, /* out */ BMDTimeValue *ticksPerFrame) = 0;
protected:
virtual ~IDeckLinkEncoderInput_v10_11 () {} // call Release method to drop reference count
};
#endif /* defined(BMD_DECKLINKAPIVIDEOENCODERINPUT_v10_11_H) */

View file

@ -0,0 +1,63 @@
/* -LICENSE-START-
** Copyright (c) 2022 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation (the
** "Software") to use, reproduce, display, distribute, sub-license, execute,
** and transmit the Software, and to prepare derivative works of the Software,
** and to permit third-parties to whom the Software is furnished to do so, in
** accordance with:
**
** (1) if the Software is obtained from Blackmagic Design, the End User License
** Agreement for the Software Development Kit (EULA) available at
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
**
** (2) if the Software is obtained from any third party, such licensing terms
** as notified by that third party,
**
** and all subject to the following:
**
** (3) the copyright notices in the Software and this entire statement,
** including the above license grant, this restriction and the following
** disclaimer, must be included in all copies of the Software, in whole or in
** part, and all derivative works of the Software, unless such copies or
** derivative works are solely in the form of machine-executable object code
** generated by a source language processor.
**
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
**
** A copy of the Software is available free of charge at
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
**
** -LICENSE-END-
*/
#ifndef BMD_DECKLINKAPIVIDEOFRAME3DEXTENSIONS_v14_2_1_H
#define BMD_DECKLINKAPIVIDEOFRAME3DEXTENSIONS_v14_2_1_H
#include "DeckLinkAPI.h"
#include "DeckLinkAPIVideoFrame_v14_2_1.h"
// Type Declarations
BMD_CONST REFIID IID_IDeckLinkVideoFrame3DExtensions_v14_2_1 = /* DA0F7E4A-EDC7-48A8-9CDD-2DB51C729CD7 */ { 0xDA, 0x0F, 0x7E, 0x4A, 0xED, 0xC7, 0x48, 0xA8, 0x9C, 0xDD, 0x2D, 0xB5, 0x1C, 0x72, 0x9C, 0xD7 };
/* Interface IDeckLinkVideoFrame3DExtensions - Optional interface implemented on IDeckLinkVideoFrame to support 3D frames */
class BMD_PUBLIC IDeckLinkVideoFrame3DExtensions_v14_2_1 : public IUnknown
{
public:
virtual BMDVideo3DPackingFormat Get3DPackingFormat (void) = 0;
virtual HRESULT GetFrameForRightEye (/* out */ IDeckLinkVideoFrame_v14_2_1** rightEyeFrame) = 0;
protected:
virtual ~IDeckLinkVideoFrame3DExtensions_v14_2_1 () {} // call Release method to drop reference count
};
#endif /* defined(BMD_DECKLINKAPIVIDEOFRAME3DEXTENSIONS_v14_2_1_H) */

View file

@ -0,0 +1,68 @@
/* -LICENSE-START-
** Copyright (c) 2022 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation (the
** "Software") to use, reproduce, display, distribute, sub-license, execute,
** and transmit the Software, and to prepare derivative works of the Software,
** and to permit third-parties to whom the Software is furnished to do so, in
** accordance with:
**
** (1) if the Software is obtained from Blackmagic Design, the End User License
** Agreement for the Software Development Kit (EULA) available at
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
**
** (2) if the Software is obtained from any third party, such licensing terms
** as notified by that third party,
**
** and all subject to the following:
**
** (3) the copyright notices in the Software and this entire statement,
** including the above license grant, this restriction and the following
** disclaimer, must be included in all copies of the Software, in whole or in
** part, and all derivative works of the Software, unless such copies or
** derivative works are solely in the form of machine-executable object code
** generated by a source language processor.
**
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
**
** A copy of the Software is available free of charge at
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
**
** -LICENSE-END-
*/
#ifndef BMD_DECKLINKAPIVIDEOFRAME_v14_2_1_H
#define BMD_DECKLINKAPIVIDEOFRAME_v14_2_1_H
#include "DeckLinkAPI.h"
// Type Declarations
BMD_CONST REFIID IID_IDeckLinkVideoFrame_v14_2_1 = /* 3F716FE0-F023-4111-BE5D-EF4414C05B17 */ { 0x3F, 0x71, 0x6F, 0xE0, 0xF0, 0x23, 0x41, 0x11, 0xBE, 0x5D, 0xEF, 0x44, 0x14, 0xC0, 0x5B, 0x17 };
/* Interface IDeckLinkVideoFrame - Interface to encapsulate a video frame; can be caller-implemented. */
class BMD_PUBLIC IDeckLinkVideoFrame_v14_2_1 : public IUnknown
{
public:
virtual long GetWidth (void) = 0;
virtual long GetHeight (void) = 0;
virtual long GetRowBytes (void) = 0;
virtual BMDPixelFormat GetPixelFormat (void) = 0;
virtual BMDFrameFlags GetFlags (void) = 0;
virtual HRESULT GetBytes (/* out */ void** buffer) = 0;
virtual HRESULT GetTimecode (/* in */ BMDTimecodeFormat format, /* out */ IDeckLinkTimecode** timecode) = 0;
virtual HRESULT GetAncillaryData (/* out */ IDeckLinkVideoFrameAncillary** ancillary) = 0; // Use of IDeckLinkVideoFrameAncillaryPackets is preferred
protected:
virtual ~IDeckLinkVideoFrame_v14_2_1 () {} // call Release method to drop reference count
};
#endif /* defined(BMD_DECKLINKAPIVIDEOFRAME_v14_2_1_H) */

View file

@ -0,0 +1,91 @@
/* -LICENSE-START-
** Copyright (c) 2017 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation (the
** "Software") to use, reproduce, display, distribute, sub-license, execute,
** and transmit the Software, and to prepare derivative works of the Software,
** and to permit third-parties to whom the Software is furnished to do so, in
** accordance with:
**
** (1) if the Software is obtained from Blackmagic Design, the End User License
** Agreement for the Software Development Kit ("EULA") available at
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
**
** (2) if the Software is obtained from any third party, such licensing terms
** as notified by that third party,
**
** and all subject to the following:
**
** (3) the copyright notices in the Software and this entire statement,
** including the above license grant, this restriction and the following
** disclaimer, must be included in all copies of the Software, in whole or in
** part, and all derivative works of the Software, unless such copies or
** derivative works are solely in the form of machine-executable object code
** generated by a source language processor.
**
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
**
** A copy of the Software is available free of charge at
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
**
** -LICENSE-END-
*/
#ifndef BMD_DECKLINKAPIVIDEOINPUT_v10_11_H
#define BMD_DECKLINKAPIVIDEOINPUT_v10_11_H
#include "DeckLinkAPI.h"
#include "DeckLinkAPI_v10_11.h"
#include "DeckLinkAPIMemoryAllocator_v14_2_1.h"
#include "DeckLinkAPIVideoInput_v11_5_1.h"
// Type Declarations
BMD_CONST REFIID IID_IDeckLinkInput_v10_11 = /* AF22762B-DFAC-4846-AA79-FA8883560995 */ {0xAF,0x22,0x76,0x2B,0xDF,0xAC,0x48,0x46,0xAA,0x79,0xFA,0x88,0x83,0x56,0x09,0x95};
/* Interface IDeckLinkInput_v10_11 - DeckLink input interface. */
class IDeckLinkInput_v10_11 : public IUnknown
{
public:
virtual HRESULT DoesSupportVideoMode (/* in */ BMDDisplayMode displayMode, /* in */ BMDPixelFormat pixelFormat, /* in */ BMDVideoInputFlags flags, /* out */ BMDDisplayModeSupport_v10_11 *result, /* out */ IDeckLinkDisplayMode **resultDisplayMode) = 0;
virtual HRESULT GetDisplayModeIterator (/* out */ IDeckLinkDisplayModeIterator **iterator) = 0;
virtual HRESULT SetScreenPreviewCallback (/* in */ IDeckLinkScreenPreviewCallback_v14_2_1 *previewCallback) = 0;
/* Video Input */
virtual HRESULT EnableVideoInput (/* in */ BMDDisplayMode displayMode, /* in */ BMDPixelFormat pixelFormat, /* in */ BMDVideoInputFlags flags) = 0;
virtual HRESULT DisableVideoInput (void) = 0;
virtual HRESULT GetAvailableVideoFrameCount (/* out */ uint32_t *availableFrameCount) = 0;
virtual HRESULT SetVideoInputFrameMemoryAllocator (/* in */ IDeckLinkMemoryAllocator_v14_2_1 *theAllocator) = 0;
/* Audio Input */
virtual HRESULT EnableAudioInput (/* in */ BMDAudioSampleRate sampleRate, /* in */ BMDAudioSampleType sampleType, /* in */ uint32_t channelCount) = 0;
virtual HRESULT DisableAudioInput (void) = 0;
virtual HRESULT GetAvailableAudioSampleFrameCount (/* out */ uint32_t *availableSampleFrameCount) = 0;
/* Input Control */
virtual HRESULT StartStreams (void) = 0;
virtual HRESULT StopStreams (void) = 0;
virtual HRESULT PauseStreams (void) = 0;
virtual HRESULT FlushStreams (void) = 0;
virtual HRESULT SetCallback (/* in */ IDeckLinkInputCallback_v11_5_1 *theCallback) = 0;
/* Hardware Timing */
virtual HRESULT GetHardwareReferenceClock (/* in */ BMDTimeScale desiredTimeScale, /* out */ BMDTimeValue *hardwareTime, /* out */ BMDTimeValue *timeInFrame, /* out */ BMDTimeValue *ticksPerFrame) = 0;
protected:
virtual ~IDeckLinkInput_v10_11 () {} // call Release method to drop reference count
};
#endif /* defined(BMD_DECKLINKAPIVIDEOINPUT_v10_11_H) */

View file

@ -0,0 +1,90 @@
/* -LICENSE-START-
** Copyright (c) 2019 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation (the
** "Software") to use, reproduce, display, distribute, sub-license, execute,
** and transmit the Software, and to prepare derivative works of the Software,
** and to permit third-parties to whom the Software is furnished to do so, in
** accordance with:
**
** (1) if the Software is obtained from Blackmagic Design, the End User License
** Agreement for the Software Development Kit ("EULA") available at
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
**
** (2) if the Software is obtained from any third party, such licensing terms
** as notified by that third party,
**
** and all subject to the following:
**
** (3) the copyright notices in the Software and this entire statement,
** including the above license grant, this restriction and the following
** disclaimer, must be included in all copies of the Software, in whole or in
** part, and all derivative works of the Software, unless such copies or
** derivative works are solely in the form of machine-executable object code
** generated by a source language processor.
**
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
**
** A copy of the Software is available free of charge at
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
**
** -LICENSE-END-
*/
#ifndef BMD_DECKLINKAPIVIDEOINPUT_v11_4_H
#define BMD_DECKLINKAPIVIDEOINPUT_v11_4_H
#include "DeckLinkAPI.h"
#include "DeckLinkAPIMemoryAllocator_v14_2_1.h"
#include "DeckLinkAPIVideoInput_v11_5_1.h"
// Type Declarations
BMD_CONST REFIID IID_IDeckLinkInput_v11_4 = /* 2A88CF76-F494-4216-A7EF-DC74EEB83882 */ { 0x2A,0x88,0xCF,0x76,0xF4,0x94,0x42,0x16,0xA7,0xEF,0xDC,0x74,0xEE,0xB8,0x38,0x82 };
/* Interface IDeckLinkInput_v11_4 - Created by QueryInterface from IDeckLink. */
class BMD_PUBLIC IDeckLinkInput_v11_4 : public IUnknown
{
public:
virtual HRESULT DoesSupportVideoMode (/* in */ BMDVideoConnection connection /* If a value of 0 is specified, the caller does not care about the connection */, /* in */ BMDDisplayMode requestedMode, /* in */ BMDPixelFormat requestedPixelFormat, /* in */ BMDSupportedVideoModeFlags flags, /* out */ bool* supported) = 0;
virtual HRESULT GetDisplayMode (/* in */ BMDDisplayMode displayMode, /* out */ IDeckLinkDisplayMode** resultDisplayMode) = 0;
virtual HRESULT GetDisplayModeIterator (/* out */ IDeckLinkDisplayModeIterator** iterator) = 0;
virtual HRESULT SetScreenPreviewCallback (/* in */ IDeckLinkScreenPreviewCallback_v14_2_1* previewCallback) = 0;
/* Video Input */
virtual HRESULT EnableVideoInput (/* in */ BMDDisplayMode displayMode, /* in */ BMDPixelFormat pixelFormat, /* in */ BMDVideoInputFlags flags) = 0;
virtual HRESULT DisableVideoInput (void) = 0;
virtual HRESULT GetAvailableVideoFrameCount (/* out */ uint32_t* availableFrameCount) = 0;
virtual HRESULT SetVideoInputFrameMemoryAllocator (/* in */ IDeckLinkMemoryAllocator_v14_2_1* theAllocator) = 0;
/* Audio Input */
virtual HRESULT EnableAudioInput (/* in */ BMDAudioSampleRate sampleRate, /* in */ BMDAudioSampleType sampleType, /* in */ uint32_t channelCount) = 0;
virtual HRESULT DisableAudioInput (void) = 0;
virtual HRESULT GetAvailableAudioSampleFrameCount (/* out */ uint32_t* availableSampleFrameCount) = 0;
/* Input Control */
virtual HRESULT StartStreams (void) = 0;
virtual HRESULT StopStreams (void) = 0;
virtual HRESULT PauseStreams (void) = 0;
virtual HRESULT FlushStreams (void) = 0;
virtual HRESULT SetCallback (/* in */ IDeckLinkInputCallback_v11_5_1* theCallback) = 0;
/* Hardware Timing */
virtual HRESULT GetHardwareReferenceClock (/* in */ BMDTimeScale desiredTimeScale, /* out */ BMDTimeValue* hardwareTime, /* out */ BMDTimeValue* timeInFrame, /* out */ BMDTimeValue* ticksPerFrame) = 0;
protected:
virtual ~IDeckLinkInput_v11_4 () {} // call Release method to drop reference count
};
#endif /* defined(BMD_DECKLINKAPIVIDEOINPUT_v11_4_H) */

View file

@ -0,0 +1,103 @@
/* -LICENSE-START-
** Copyright (c) 2020 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation (the
** "Software") to use, reproduce, display, distribute, sub-license, execute,
** and transmit the Software, and to prepare derivative works of the Software,
** and to permit third-parties to whom the Software is furnished to do so, in
** accordance with:
**
** (1) if the Software is obtained from Blackmagic Design, the End User License
** Agreement for the Software Development Kit ("EULA") available at
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
**
** (2) if the Software is obtained from any third party, such licensing terms
** as notified by that third party,
**
** and all subject to the following:
**
** (3) the copyright notices in the Software and this entire statement,
** including the above license grant, this restriction and the following
** disclaimer, must be included in all copies of the Software, in whole or in
** part, and all derivative works of the Software, unless such copies or
** derivative works are solely in the form of machine-executable object code
** generated by a source language processor.
**
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
**
** A copy of the Software is available free of charge at
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
**
** -LICENSE-END-
*/
#ifndef BMD_DECKLINKAPIVIDEOINPUT_v11_5_1_H
#define BMD_DECKLINKAPIVIDEOINPUT_v11_5_1_H
#include "DeckLinkAPI.h"
#include "DeckLinkAPIVideoInput_v14_2_1.h"
// Type Declarations
BMD_CONST REFIID IID_IDeckLinkInputCallback_v11_5_1 = /* DD04E5EC-7415-42AB-AE4A-E80C4DFC044A */ { 0xDD, 0x04, 0xE5, 0xEC, 0x74, 0x15, 0x42, 0xAB, 0xAE, 0x4A, 0xE8, 0x0C, 0x4D, 0xFC, 0x04, 0x4A };
BMD_CONST REFIID IID_IDeckLinkInput_v11_5_1 = /* 9434C6E4-B15D-4B1C-979E-661E3DDCB4B9 */ { 0x94, 0x34, 0xC6, 0xE4, 0xB1, 0x5D, 0x4B, 0x1C, 0x97, 0x9E, 0x66, 0x1E, 0x3D, 0xDC, 0xB4, 0xB9 };
/* Interface IDeckLinkInputCallback_v11_5_1 - Frame arrival callback. */
class BMD_PUBLIC IDeckLinkInputCallback_v11_5_1 : public IUnknown
{
public:
virtual HRESULT VideoInputFormatChanged (/* in */ BMDVideoInputFormatChangedEvents notificationEvents, /* in */ IDeckLinkDisplayMode* newDisplayMode, /* in */ BMDDetectedVideoInputFormatFlags detectedSignalFlags) = 0;
virtual HRESULT VideoInputFrameArrived (/* in */ IDeckLinkVideoInputFrame_v14_2_1* videoFrame, /* in */ IDeckLinkAudioInputPacket* audioPacket) = 0;
protected:
virtual ~IDeckLinkInputCallback_v11_5_1 () {} // call Release method to drop reference count
};
/* Interface IDeckLinkInput_v11_5_1 - Created by QueryInterface from IDeckLink. */
class BMD_PUBLIC IDeckLinkInput_v11_5_1 : public IUnknown
{
public:
virtual HRESULT DoesSupportVideoMode (/* in */ BMDVideoConnection connection /* If a value of bmdVideoConnectionUnspecified is specified, the caller does not care about the connection */, /* in */ BMDDisplayMode requestedMode, /* in */ BMDPixelFormat requestedPixelFormat, /* in */ BMDVideoInputConversionMode conversionMode, /* in */ BMDSupportedVideoModeFlags flags, /* out */ BMDDisplayMode* actualMode, /* out */ bool* supported) = 0;
virtual HRESULT GetDisplayMode (/* in */ BMDDisplayMode displayMode, /* out */ IDeckLinkDisplayMode** resultDisplayMode) = 0;
virtual HRESULT GetDisplayModeIterator (/* out */ IDeckLinkDisplayModeIterator** iterator) = 0;
virtual HRESULT SetScreenPreviewCallback (/* in */ IDeckLinkScreenPreviewCallback_v14_2_1* previewCallback) = 0;
/* Video Input */
virtual HRESULT EnableVideoInput (/* in */ BMDDisplayMode displayMode, /* in */ BMDPixelFormat pixelFormat, /* in */ BMDVideoInputFlags flags) = 0;
virtual HRESULT DisableVideoInput (void) = 0;
virtual HRESULT GetAvailableVideoFrameCount (/* out */ uint32_t* availableFrameCount) = 0;
virtual HRESULT SetVideoInputFrameMemoryAllocator (/* in */ IDeckLinkMemoryAllocator_v14_2_1* theAllocator) = 0;
/* Audio Input */
virtual HRESULT EnableAudioInput (/* in */ BMDAudioSampleRate sampleRate, /* in */ BMDAudioSampleType sampleType, /* in */ uint32_t channelCount) = 0;
virtual HRESULT DisableAudioInput (void) = 0;
virtual HRESULT GetAvailableAudioSampleFrameCount (/* out */ uint32_t* availableSampleFrameCount) = 0;
/* Input Control */
virtual HRESULT StartStreams (void) = 0;
virtual HRESULT StopStreams (void) = 0;
virtual HRESULT PauseStreams (void) = 0;
virtual HRESULT FlushStreams (void) = 0;
virtual HRESULT SetCallback (/* in */ IDeckLinkInputCallback_v11_5_1* theCallback) = 0;
/* Hardware Timing */
virtual HRESULT GetHardwareReferenceClock (/* in */ BMDTimeScale desiredTimeScale, /* out */ BMDTimeValue* hardwareTime, /* out */ BMDTimeValue* timeInFrame, /* out */ BMDTimeValue* ticksPerFrame) = 0;
protected:
virtual ~IDeckLinkInput_v11_5_1 () {} // call Release method to drop reference count
};
#endif /* defined(BMD_DECKLINKAPIVIDEOINPUT_v11_5_1_H) */

View file

@ -0,0 +1,118 @@
/* -LICENSE-START-
** Copyright (c) 2022 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation (the
** "Software") to use, reproduce, display, distribute, sub-license, execute,
** and transmit the Software, and to prepare derivative works of the Software,
** and to permit third-parties to whom the Software is furnished to do so, in
** accordance with:
**
** (1) if the Software is obtained from Blackmagic Design, the End User License
** Agreement for the Software Development Kit (EULA) available at
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
**
** (2) if the Software is obtained from any third party, such licensing terms
** as notified by that third party,
**
** and all subject to the following:
**
** (3) the copyright notices in the Software and this entire statement,
** including the above license grant, this restriction and the following
** disclaimer, must be included in all copies of the Software, in whole or in
** part, and all derivative works of the Software, unless such copies or
** derivative works are solely in the form of machine-executable object code
** generated by a source language processor.
**
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
**
** A copy of the Software is available free of charge at
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
**
** -LICENSE-END-
*/
#ifndef BMD_DECKLINKAPIVIDEOINPUT_v14_2_1_H
#define BMD_DECKLINKAPIVIDEOINPUT_v14_2_1_H
#include "DeckLinkAPI.h"
#include "DeckLinkAPIMemoryAllocator_v14_2_1.h"
#include "DeckLinkAPIVideoFrame_v14_2_1.h"
#include "DeckLinkAPIScreenPreviewCallback_v14_2_1.h"
// Type Declarations
BMD_CONST REFIID IID_IDeckLinkVideoInputFrame_v14_2_1 = /* 05CFE374-537C-4094-9A57-680525118F44 */ { 0x05, 0xCF, 0xE3, 0x74, 0x53, 0x7C, 0x40, 0x94, 0x9A, 0x57, 0x68, 0x05, 0x25, 0x11, 0x8F, 0x44 };
BMD_CONST REFIID IID_IDeckLinkInputCallback_v14_2_1 = /* C6FCE4C9-C4E4-4047-82FB-5D238232A902 */ { 0xC6, 0xFC, 0xE4, 0xC9, 0xC4, 0xE4, 0x40, 0x47, 0x82, 0xFB, 0x5D, 0x23, 0x82, 0x32, 0xA9, 0x02 };
BMD_CONST REFIID IID_IDeckLinkInput_v14_2_1 = /* C21CDB6E-F414-46E4-A636-80A566E0ED37 */ { 0xC2, 0x1C, 0xDB, 0x6E, 0xF4, 0x14, 0x46, 0xE4, 0xA6, 0x36, 0x80, 0xA5, 0x66, 0xE0, 0xED, 0x37 };
/* Interface IDeckLinkVideoInputFrame - Provided by the IDeckLinkVideoInput frame arrival callback. */
class BMD_PUBLIC IDeckLinkVideoInputFrame_v14_2_1 : public IDeckLinkVideoFrame_v14_2_1
{
public:
virtual HRESULT GetStreamTime (/* out */ BMDTimeValue* frameTime, /* out */ BMDTimeValue* frameDuration, /* in */ BMDTimeScale timeScale) = 0;
virtual HRESULT GetHardwareReferenceTimestamp (/* in */ BMDTimeScale timeScale, /* out */ BMDTimeValue* frameTime, /* out */ BMDTimeValue* frameDuration) = 0;
protected:
virtual ~IDeckLinkVideoInputFrame_v14_2_1 () {} // call Release method to drop reference count
};
/* Interface IDeckLinkInputCallback_v14_2_1 - Frame arrival callback. */
class BMD_PUBLIC IDeckLinkInputCallback_v14_2_1 : public IUnknown
{
public:
virtual HRESULT VideoInputFormatChanged (/* in */ BMDVideoInputFormatChangedEvents notificationEvents, /* in */ IDeckLinkDisplayMode* newDisplayMode, /* in */ BMDDetectedVideoInputFormatFlags detectedSignalFlags) = 0;
virtual HRESULT VideoInputFrameArrived (/* in */ IDeckLinkVideoInputFrame_v14_2_1* videoFrame, /* in */ IDeckLinkAudioInputPacket* audioPacket) = 0;
protected:
virtual ~IDeckLinkInputCallback_v14_2_1 () {} // call Release method to drop reference count
};
/* Interface IDeckLinkInput - Created by QueryInterface from IDeckLink. */
class BMD_PUBLIC IDeckLinkInput_v14_2_1 : public IUnknown
{
public:
virtual HRESULT DoesSupportVideoMode (/* in */ BMDVideoConnection connection /* If a value of bmdVideoConnectionUnspecified is specified, the caller does not care about the connection */, /* in */ BMDDisplayMode requestedMode, /* in */ BMDPixelFormat requestedPixelFormat, /* in */ BMDVideoInputConversionMode conversionMode, /* in */ BMDSupportedVideoModeFlags flags, /* out */ BMDDisplayMode* actualMode, /* out */ bool* supported) = 0;
virtual HRESULT GetDisplayMode (/* in */ BMDDisplayMode displayMode, /* out */ IDeckLinkDisplayMode** resultDisplayMode) = 0;
virtual HRESULT GetDisplayModeIterator (/* out */ IDeckLinkDisplayModeIterator** iterator) = 0;
virtual HRESULT SetScreenPreviewCallback (/* in */ IDeckLinkScreenPreviewCallback_v14_2_1* previewCallback) = 0;
/* Video Input */
virtual HRESULT EnableVideoInput (/* in */ BMDDisplayMode displayMode, /* in */ BMDPixelFormat pixelFormat, /* in */ BMDVideoInputFlags flags) = 0;
virtual HRESULT DisableVideoInput (void) = 0;
virtual HRESULT GetAvailableVideoFrameCount (/* out */ uint32_t* availableFrameCount) = 0;
virtual HRESULT SetVideoInputFrameMemoryAllocator (/* in */ IDeckLinkMemoryAllocator_v14_2_1* theAllocator) = 0;
/* Audio Input */
virtual HRESULT EnableAudioInput (/* in */ BMDAudioSampleRate sampleRate, /* in */ BMDAudioSampleType sampleType, /* in */ uint32_t channelCount) = 0;
virtual HRESULT DisableAudioInput (void) = 0;
virtual HRESULT GetAvailableAudioSampleFrameCount (/* out */ uint32_t* availableSampleFrameCount) = 0;
/* Input Control */
virtual HRESULT StartStreams (void) = 0;
virtual HRESULT StopStreams (void) = 0;
virtual HRESULT PauseStreams (void) = 0;
virtual HRESULT FlushStreams (void) = 0;
virtual HRESULT SetCallback (/* in */ IDeckLinkInputCallback_v14_2_1* theCallback) = 0;
/* Hardware Timing */
virtual HRESULT GetHardwareReferenceClock (/* in */ BMDTimeScale desiredTimeScale, /* out */ BMDTimeValue* hardwareTime, /* out */ BMDTimeValue* timeInFrame, /* out */ BMDTimeValue* ticksPerFrame) = 0;
protected:
virtual ~IDeckLinkInput_v14_2_1 () {} // call Release method to drop reference count
};
#endif /* defined(BMD_DECKLINKAPIVIDEOINPUT_v14_2_1_H) */

View file

@ -0,0 +1,86 @@
/* -LICENSE-START-
** Copyright (c) 2025 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation (the
** "Software") to use, reproduce, display, distribute, sub-license, execute,
** and transmit the Software, and to prepare derivative works of the Software,
** and to permit third-parties to whom the Software is furnished to do so, in
** accordance with:
**
** (1) if the Software is obtained from Blackmagic Design, the End User License
** Agreement for the Software Development Kit (EULA) available at
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
**
** (2) if the Software is obtained from any third party, such licensing terms
** as notified by that third party,
**
** and all subject to the following:
**
** (3) the copyright notices in the Software and this entire statement,
** including the above license grant, this restriction and the following
** disclaimer, must be included in all copies of the Software, in whole or in
** part, and all derivative works of the Software, unless such copies or
** derivative works are solely in the form of machine-executable object code
** generated by a source language processor.
**
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
**
** A copy of the Software is available free of charge at
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
**
** -LICENSE-END-
*/
#pragma once
#include "DeckLinkAPI_v15_3_1.h"
// Type Declarations
BMD_CONST REFIID IID_IDeckLinkInput_v15_3_1 = /* 4095DB82-E294-4B8C-AAA8-3B9E80C49336 */ { 0x40,0x95,0xDB,0x82,0xE2,0x94,0x4B,0x8C,0xAA,0xA8,0x3B,0x9E,0x80,0xC4,0x93,0x36 };
/* Interface IDeckLinkInput_v15_3_1 - Created by QueryInterface from IDeckLink. */
class BMD_PUBLIC IDeckLinkInput_v15_3_1 : public IUnknown
{
public:
virtual HRESULT DoesSupportVideoMode (/* in */ BMDVideoConnection connection /* If a value of bmdVideoConnectionUnspecified is specified, the caller does not care about the connection */, /* in */ BMDDisplayMode requestedMode, /* in */ BMDPixelFormat requestedPixelFormat, /* in */ BMDVideoInputConversionMode conversionMode, /* in */ BMDSupportedVideoModeFlags flags, /* out */ BMDDisplayMode* actualMode, /* out */ bool* supported) = 0;
virtual HRESULT GetDisplayMode (/* in */ BMDDisplayMode displayMode, /* out */ IDeckLinkDisplayMode** resultDisplayMode) = 0;
virtual HRESULT GetDisplayModeIterator (/* out */ IDeckLinkDisplayModeIterator** iterator) = 0;
virtual HRESULT SetScreenPreviewCallback (/* in */ IDeckLinkScreenPreviewCallback* previewCallback) = 0;
/* Video Input */
virtual HRESULT EnableVideoInput (/* in */ BMDDisplayMode displayMode, /* in */ BMDPixelFormat pixelFormat, /* in */ BMDVideoInputFlags flags) = 0;
virtual HRESULT EnableVideoInputWithAllocatorProvider (/* in */ BMDDisplayMode displayMode, /* in */ BMDPixelFormat pixelFormat, /* in */ BMDVideoInputFlags flags, /* in */ IDeckLinkVideoBufferAllocatorProvider_v15_3_1* allocatorProvider) = 0;
virtual HRESULT DisableVideoInput (void) = 0;
virtual HRESULT GetAvailableVideoFrameCount (/* out */ uint32_t* availableFrameCount) = 0;
/* Audio Input */
virtual HRESULT EnableAudioInput (/* in */ BMDAudioSampleRate sampleRate, /* in */ BMDAudioSampleType sampleType, /* in */ uint32_t channelCount) = 0;
virtual HRESULT DisableAudioInput (void) = 0;
virtual HRESULT GetAvailableAudioSampleFrameCount (/* out */ uint32_t* availableSampleFrameCount) = 0;
/* Input Control */
virtual HRESULT StartStreams (void) = 0;
virtual HRESULT StopStreams (void) = 0;
virtual HRESULT PauseStreams (void) = 0;
virtual HRESULT FlushStreams (void) = 0;
virtual HRESULT SetCallback (/* in */ IDeckLinkInputCallback* theCallback) = 0;
/* Hardware Timing */
virtual HRESULT GetHardwareReferenceClock (/* in */ BMDTimeScale desiredTimeScale, /* out */ BMDTimeValue* hardwareTime, /* out */ BMDTimeValue* timeInFrame, /* out */ BMDTimeValue* ticksPerFrame) = 0;
protected:
virtual ~IDeckLinkInput_v15_3_1 () {} // call Release method to drop reference count
};

View file

@ -0,0 +1,109 @@
/* -LICENSE-START-
** Copyright (c) 2017 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation (the
** "Software") to use, reproduce, display, distribute, sub-license, execute,
** and transmit the Software, and to prepare derivative works of the Software,
** and to permit third-parties to whom the Software is furnished to do so, in
** accordance with:
**
** (1) if the Software is obtained from Blackmagic Design, the End User License
** Agreement for the Software Development Kit ("EULA") available at
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
**
** (2) if the Software is obtained from any third party, such licensing terms
** as notified by that third party,
**
** and all subject to the following:
**
** (3) the copyright notices in the Software and this entire statement,
** including the above license grant, this restriction and the following
** disclaimer, must be included in all copies of the Software, in whole or in
** part, and all derivative works of the Software, unless such copies or
** derivative works are solely in the form of machine-executable object code
** generated by a source language processor.
**
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
**
** A copy of the Software is available free of charge at
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
**
** -LICENSE-END-
*/
#ifndef BMD_DECKLINKAPIVIDEOOUTPUT_v10_11_H
#define BMD_DECKLINKAPIVIDEOOUTPUT_v10_11_H
#include "DeckLinkAPI.h"
#include "DeckLinkAPI_v10_11.h"
#include "DeckLinkAPIVideoInput_v14_2_1.h"
#include "DeckLinkAPIVideoOutput_v14_2_1.h"
// Type Declarations
BMD_CONST REFIID IID_IDeckLinkOutput_v10_11 = /* CC5C8A6E-3F2F-4B3A-87EA-FD78AF300564 */ {0xCC,0x5C,0x8A,0x6E,0x3F,0x2F,0x4B,0x3A,0x87,0xEA,0xFD,0x78,0xAF,0x30,0x05,0x64};
/* Interface IDeckLinkOutput_v10_11 - DeckLink output interface. */
class IDeckLinkOutput_v10_11 : public IUnknown
{
public:
virtual HRESULT DoesSupportVideoMode (/* in */ BMDDisplayMode displayMode, /* in */ BMDPixelFormat pixelFormat, /* in */ BMDVideoOutputFlags flags, /* out */ BMDDisplayModeSupport_v10_11 *result, /* out */ IDeckLinkDisplayMode **resultDisplayMode) = 0;
virtual HRESULT GetDisplayModeIterator (/* out */ IDeckLinkDisplayModeIterator **iterator) = 0;
virtual HRESULT SetScreenPreviewCallback (/* in */ IDeckLinkScreenPreviewCallback_v14_2_1 *previewCallback) = 0;
/* Video Output */
virtual HRESULT EnableVideoOutput (/* in */ BMDDisplayMode displayMode, /* in */ BMDVideoOutputFlags flags) = 0;
virtual HRESULT DisableVideoOutput (void) = 0;
virtual HRESULT SetVideoOutputFrameMemoryAllocator (/* in */ IDeckLinkMemoryAllocator_v14_2_1 *theAllocator) = 0;
virtual HRESULT CreateVideoFrame (/* in */ int32_t width, /* in */ int32_t height, /* in */ int32_t rowBytes, /* in */ BMDPixelFormat pixelFormat, /* in */ BMDFrameFlags flags, /* out */ IDeckLinkMutableVideoFrame_v14_2_1 **outFrame) = 0;
virtual HRESULT CreateAncillaryData (/* in */ BMDPixelFormat pixelFormat, /* out */ IDeckLinkVideoFrameAncillary **outBuffer) = 0;
virtual HRESULT DisplayVideoFrameSync (/* in */ IDeckLinkVideoFrame_v14_2_1 *theFrame) = 0;
virtual HRESULT ScheduleVideoFrame (/* in */ IDeckLinkVideoFrame_v14_2_1 *theFrame, /* in */ BMDTimeValue displayTime, /* in */ BMDTimeValue displayDuration, /* in */ BMDTimeScale timeScale) = 0;
virtual HRESULT SetScheduledFrameCompletionCallback (/* in */ IDeckLinkVideoOutputCallback_v14_2_1 *theCallback) = 0;
virtual HRESULT GetBufferedVideoFrameCount (/* out */ uint32_t *bufferedFrameCount) = 0;
/* Audio Output */
virtual HRESULT EnableAudioOutput (/* in */ BMDAudioSampleRate sampleRate, /* in */ BMDAudioSampleType sampleType, /* in */ uint32_t channelCount, /* in */ BMDAudioOutputStreamType streamType) = 0;
virtual HRESULT DisableAudioOutput (void) = 0;
virtual HRESULT WriteAudioSamplesSync (/* in */ void *buffer, /* in */ uint32_t sampleFrameCount, /* out */ uint32_t *sampleFramesWritten) = 0;
virtual HRESULT BeginAudioPreroll (void) = 0;
virtual HRESULT EndAudioPreroll (void) = 0;
virtual HRESULT ScheduleAudioSamples (/* in */ void *buffer, /* in */ uint32_t sampleFrameCount, /* in */ BMDTimeValue streamTime, /* in */ BMDTimeScale timeScale, /* out */ uint32_t *sampleFramesWritten) = 0;
virtual HRESULT GetBufferedAudioSampleFrameCount (/* out */ uint32_t *bufferedSampleFrameCount) = 0;
virtual HRESULT FlushBufferedAudioSamples (void) = 0;
virtual HRESULT SetAudioCallback (/* in */ IDeckLinkAudioOutputCallback *theCallback) = 0;
/* Output Control */
virtual HRESULT StartScheduledPlayback (/* in */ BMDTimeValue playbackStartTime, /* in */ BMDTimeScale timeScale, /* in */ double playbackSpeed) = 0;
virtual HRESULT StopScheduledPlayback (/* in */ BMDTimeValue stopPlaybackAtTime, /* out */ BMDTimeValue *actualStopTime, /* in */ BMDTimeScale timeScale) = 0;
virtual HRESULT IsScheduledPlaybackRunning (/* out */ bool *active) = 0;
virtual HRESULT GetScheduledStreamTime (/* in */ BMDTimeScale desiredTimeScale, /* out */ BMDTimeValue *streamTime, /* out */ double *playbackSpeed) = 0;
virtual HRESULT GetReferenceStatus (/* out */ BMDReferenceStatus *referenceStatus) = 0;
/* Hardware Timing */
virtual HRESULT GetHardwareReferenceClock (/* in */ BMDTimeScale desiredTimeScale, /* out */ BMDTimeValue *hardwareTime, /* out */ BMDTimeValue *timeInFrame, /* out */ BMDTimeValue *ticksPerFrame) = 0;
virtual HRESULT GetFrameCompletionReferenceTimestamp (/* in */ IDeckLinkVideoFrame_v14_2_1 *theFrame, /* in */ BMDTimeScale desiredTimeScale, /* out */ BMDTimeValue *frameCompletionTimestamp) = 0;
protected:
virtual ~IDeckLinkOutput_v10_11 () {} // call Release method to drop reference count
};
#endif /* defined(BMD_DECKLINKAPIVIDEOOUTPUT_v10_11_H) */

View file

@ -0,0 +1,101 @@
/* -LICENSE-START-
** Copyright (c) 2019 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation (the
** "Software") to use, reproduce, display, distribute, sub-license, execute,
** and transmit the Software, and to prepare derivative works of the Software,
** and to permit third-parties to whom the Software is furnished to do so, in
** accordance with:
**
** (1) if the Software is obtained from Blackmagic Design, the End User License
** Agreement for the Software Development Kit ("EULA") available at
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
**
** (2) if the Software is obtained from any third party, such licensing terms
** as notified by that third party,
**
** and all subject to the following:
**
** (3) the copyright notices in the Software and this entire statement,
** including the above license grant, this restriction and the following
** disclaimer, must be included in all copies of the Software, in whole or in
** part, and all derivative works of the Software, unless such copies or
** derivative works are solely in the form of machine-executable object code
** generated by a source language processor.
**
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
**
** A copy of the Software is available free of charge at
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
**
** -LICENSE-END-
*/
#ifndef BMD_DECKLINKAPIVIDEOOUTPUT_v11_4_H
#define BMD_DECKLINKAPIVIDEOOUTPUT_v11_4_H
#include "DeckLinkAPI.h"
#include "DeckLinkAPIVideoOutput_v14_2_1.h"
// Type Declarations
BMD_CONST REFIID IID_IDeckLinkOutput_v11_4 = /* 065A0F6C-C508-4D0D-B919-F5EB0EBFC96B */ { 0x06,0x5A,0x0F,0x6C,0xC5,0x08,0x4D,0x0D,0xB9,0x19,0xF5,0xEB,0x0E,0xBF,0xC9,0x6B };
/* Interface IDeckLinkOutput_v11_4 - Created by QueryInterface from IDeckLink. */
class BMD_PUBLIC IDeckLinkOutput_v11_4 : public IUnknown
{
public:
virtual HRESULT DoesSupportVideoMode (/* in */ BMDVideoConnection connection /* If a value of 0 is specified, the caller does not care about the connection */, /* in */ BMDDisplayMode requestedMode, /* in */ BMDPixelFormat requestedPixelFormat, /* in */ BMDSupportedVideoModeFlags flags, /* out */ BMDDisplayMode* actualMode, /* out */ bool* supported) = 0;
virtual HRESULT GetDisplayMode (/* in */ BMDDisplayMode displayMode, /* out */ IDeckLinkDisplayMode** resultDisplayMode) = 0;
virtual HRESULT GetDisplayModeIterator (/* out */ IDeckLinkDisplayModeIterator** iterator) = 0;
virtual HRESULT SetScreenPreviewCallback (/* in */ IDeckLinkScreenPreviewCallback_v14_2_1* previewCallback) = 0;
/* Video Output */
virtual HRESULT EnableVideoOutput (/* in */ BMDDisplayMode displayMode, /* in */ BMDVideoOutputFlags flags) = 0;
virtual HRESULT DisableVideoOutput (void) = 0;
virtual HRESULT SetVideoOutputFrameMemoryAllocator (/* in */ IDeckLinkMemoryAllocator_v14_2_1* theAllocator) = 0;
virtual HRESULT CreateVideoFrame (/* in */ int32_t width, /* in */ int32_t height, /* in */ int32_t rowBytes, /* in */ BMDPixelFormat pixelFormat, /* in */ BMDFrameFlags flags, /* out */ IDeckLinkMutableVideoFrame_v14_2_1** outFrame) = 0;
virtual HRESULT CreateAncillaryData (/* in */ BMDPixelFormat pixelFormat, /* out */ IDeckLinkVideoFrameAncillary** outBuffer) = 0; // Use of IDeckLinkVideoFrameAncillaryPackets is preferred
virtual HRESULT DisplayVideoFrameSync (/* in */ IDeckLinkVideoFrame_v14_2_1* theFrame) = 0;
virtual HRESULT ScheduleVideoFrame (/* in */ IDeckLinkVideoFrame_v14_2_1* theFrame, /* in */ BMDTimeValue displayTime, /* in */ BMDTimeValue displayDuration, /* in */ BMDTimeScale timeScale) = 0;
virtual HRESULT SetScheduledFrameCompletionCallback (/* in */ IDeckLinkVideoOutputCallback_v14_2_1* theCallback) = 0;
virtual HRESULT GetBufferedVideoFrameCount (/* out */ uint32_t* bufferedFrameCount) = 0;
/* Audio Output */
virtual HRESULT EnableAudioOutput (/* in */ BMDAudioSampleRate sampleRate, /* in */ BMDAudioSampleType sampleType, /* in */ uint32_t channelCount, /* in */ BMDAudioOutputStreamType streamType) = 0;
virtual HRESULT DisableAudioOutput (void) = 0;
virtual HRESULT WriteAudioSamplesSync (/* in */ void* buffer, /* in */ uint32_t sampleFrameCount, /* out */ uint32_t* sampleFramesWritten) = 0;
virtual HRESULT BeginAudioPreroll (void) = 0;
virtual HRESULT EndAudioPreroll (void) = 0;
virtual HRESULT ScheduleAudioSamples (/* in */ void* buffer, /* in */ uint32_t sampleFrameCount, /* in */ BMDTimeValue streamTime, /* in */ BMDTimeScale timeScale, /* out */ uint32_t* sampleFramesWritten) = 0;
virtual HRESULT GetBufferedAudioSampleFrameCount (/* out */ uint32_t* bufferedSampleFrameCount) = 0;
virtual HRESULT FlushBufferedAudioSamples (void) = 0;
virtual HRESULT SetAudioCallback (/* in */ IDeckLinkAudioOutputCallback* theCallback) = 0;
/* Output Control */
virtual HRESULT StartScheduledPlayback (/* in */ BMDTimeValue playbackStartTime, /* in */ BMDTimeScale timeScale, /* in */ double playbackSpeed) = 0;
virtual HRESULT StopScheduledPlayback (/* in */ BMDTimeValue stopPlaybackAtTime, /* out */ BMDTimeValue* actualStopTime, /* in */ BMDTimeScale timeScale) = 0;
virtual HRESULT IsScheduledPlaybackRunning (/* out */ bool* active) = 0;
virtual HRESULT GetScheduledStreamTime (/* in */ BMDTimeScale desiredTimeScale, /* out */ BMDTimeValue* streamTime, /* out */ double* playbackSpeed) = 0;
virtual HRESULT GetReferenceStatus (/* out */ BMDReferenceStatus* referenceStatus) = 0;
/* Hardware Timing */
virtual HRESULT GetHardwareReferenceClock (/* in */ BMDTimeScale desiredTimeScale, /* out */ BMDTimeValue* hardwareTime, /* out */ BMDTimeValue* timeInFrame, /* out */ BMDTimeValue* ticksPerFrame) = 0;
virtual HRESULT GetFrameCompletionReferenceTimestamp (/* in */ IDeckLinkVideoFrame_v14_2_1* theFrame, /* in */ BMDTimeScale desiredTimeScale, /* out */ BMDTimeValue* frameCompletionTimestamp) = 0;
protected:
virtual ~IDeckLinkOutput_v11_4 () {} // call Release method to drop reference count
};
#endif /* defined(BMD_DECKLINKAPIVIDEOOUTPUT_v11_4_H) */

View file

@ -0,0 +1,133 @@
/* -LICENSE-START-
** Copyright (c) 2022 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation (the
** "Software") to use, reproduce, display, distribute, sub-license, execute,
** and transmit the Software, and to prepare derivative works of the Software,
** and to permit third-parties to whom the Software is furnished to do so, in
** accordance with:
**
** (1) if the Software is obtained from Blackmagic Design, the End User License
** Agreement for the Software Development Kit (EULA) available at
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
**
** (2) if the Software is obtained from any third party, such licensing terms
** as notified by that third party,
**
** and all subject to the following:
**
** (3) the copyright notices in the Software and this entire statement,
** including the above license grant, this restriction and the following
** disclaimer, must be included in all copies of the Software, in whole or in
** part, and all derivative works of the Software, unless such copies or
** derivative works are solely in the form of machine-executable object code
** generated by a source language processor.
**
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
**
** A copy of the Software is available free of charge at
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
**
** -LICENSE-END-
*/
#ifndef BMD_DECKLINKAPIVIDEOOUTPUT_v14_2_1_H
#define BMD_DECKLINKAPIVIDEOOUTPUT_v14_2_1_H
#include "DeckLinkAPI.h"
#include "DeckLinkAPIMemoryAllocator_v14_2_1.h"
#include "DeckLinkAPIVideoFrame_v14_2_1.h"
#include "DeckLinkAPIScreenPreviewCallback_v14_2_1.h"
// Type Declarations
BMD_CONST REFIID IID_IDeckLinkMutableVideoFrame_v14_2_1 = /* 69E2639F-40DA-4E19-B6F2-20ACE815C390 */ { 0x69, 0xE2, 0x63, 0x9F, 0x40, 0xDA, 0x4E, 0x19, 0xB6, 0xF2, 0x20, 0xAC, 0xE8, 0x15, 0xC3, 0x90 };
BMD_CONST REFIID IID_IDeckLinkVideoOutputCallback_v14_2_1 = /* 20AA5225-1958-47CB-820B-80A8D521A6EE */ { 0x20, 0xAA, 0x52, 0x25, 0x19, 0x58, 0x47, 0xCB, 0x82, 0x0B, 0x80, 0xA8, 0xD5, 0x21, 0xA6, 0xEE };
BMD_CONST REFIID IID_IDeckLinkOutput_v14_2_1 = /* BE2D9020-461E-442F-84B7-E949CB953B9D */ { 0xBE, 0x2D, 0x90, 0x20, 0x46, 0x1E, 0x44, 0x2F, 0x84, 0xB7, 0xE9, 0x49, 0xCB, 0x95, 0x3B, 0x9D };
/* Interface IDeckLinkMutableVideoFrame - Created by IDeckLinkOutput::CreateVideoFrame. */
class BMD_PUBLIC IDeckLinkMutableVideoFrame_v14_2_1 : public IDeckLinkVideoFrame_v14_2_1
{
public:
virtual HRESULT SetFlags (/* in */ BMDFrameFlags newFlags) = 0;
virtual HRESULT SetTimecode (/* in */ BMDTimecodeFormat format, /* in */ IDeckLinkTimecode* timecode) = 0;
virtual HRESULT SetTimecodeFromComponents (/* in */ BMDTimecodeFormat format, /* in */ uint8_t hours, /* in */ uint8_t minutes, /* in */ uint8_t seconds, /* in */ uint8_t frames, /* in */ BMDTimecodeFlags flags) = 0;
virtual HRESULT SetAncillaryData (/* in */ IDeckLinkVideoFrameAncillary* ancillary) = 0;
virtual HRESULT SetTimecodeUserBits (/* in */ BMDTimecodeFormat format, /* in */ BMDTimecodeUserBits userBits) = 0;
protected:
virtual ~IDeckLinkMutableVideoFrame_v14_2_1 () {} // call Release method to drop reference count
};
/* Interface IDeckLinkVideoOutputCallback - Frame completion callback. */
class BMD_PUBLIC IDeckLinkVideoOutputCallback_v14_2_1 : public IUnknown
{
public:
virtual HRESULT ScheduledFrameCompleted (/* in */ IDeckLinkVideoFrame_v14_2_1* completedFrame, /* in */ BMDOutputFrameCompletionResult result) = 0;
virtual HRESULT ScheduledPlaybackHasStopped (void) = 0;
protected:
virtual ~IDeckLinkVideoOutputCallback_v14_2_1 () {} // call Release method to drop reference count
};
/* Interface IDeckLinkOutput - Created by QueryInterface from IDeckLink. */
class BMD_PUBLIC IDeckLinkOutput_v14_2_1 : public IUnknown
{
public:
virtual HRESULT DoesSupportVideoMode (/* in */ BMDVideoConnection connection /* If a value of bmdVideoConnectionUnspecified is specified, the caller does not care about the connection */, /* in */ BMDDisplayMode requestedMode, /* in */ BMDPixelFormat requestedPixelFormat, /* in */ BMDVideoOutputConversionMode conversionMode, /* in */ BMDSupportedVideoModeFlags flags, /* out */ BMDDisplayMode* actualMode, /* out */ bool* supported) = 0;
virtual HRESULT GetDisplayMode (/* in */ BMDDisplayMode displayMode, /* out */ IDeckLinkDisplayMode** resultDisplayMode) = 0;
virtual HRESULT GetDisplayModeIterator (/* out */ IDeckLinkDisplayModeIterator** iterator) = 0;
virtual HRESULT SetScreenPreviewCallback (/* in */ IDeckLinkScreenPreviewCallback_v14_2_1* previewCallback) = 0;
/* Video Output */
virtual HRESULT EnableVideoOutput (/* in */ BMDDisplayMode displayMode, /* in */ BMDVideoOutputFlags flags) = 0;
virtual HRESULT DisableVideoOutput (void) = 0;
virtual HRESULT SetVideoOutputFrameMemoryAllocator (/* in */ IDeckLinkMemoryAllocator_v14_2_1* theAllocator) = 0;
virtual HRESULT CreateVideoFrame (/* in */ int32_t width, /* in */ int32_t height, /* in */ int32_t rowBytes, /* in */ BMDPixelFormat pixelFormat, /* in */ BMDFrameFlags flags, /* out */ IDeckLinkMutableVideoFrame_v14_2_1** outFrame) = 0;
virtual HRESULT CreateAncillaryData (/* in */ BMDPixelFormat pixelFormat, /* out */ IDeckLinkVideoFrameAncillary** outBuffer) = 0; // Use of IDeckLinkVideoFrameAncillaryPackets is preferred
virtual HRESULT DisplayVideoFrameSync (/* in */ IDeckLinkVideoFrame_v14_2_1* theFrame) = 0;
virtual HRESULT ScheduleVideoFrame (/* in */ IDeckLinkVideoFrame_v14_2_1* theFrame, /* in */ BMDTimeValue displayTime, /* in */ BMDTimeValue displayDuration, /* in */ BMDTimeScale timeScale) = 0;
virtual HRESULT SetScheduledFrameCompletionCallback (/* in */ IDeckLinkVideoOutputCallback_v14_2_1* theCallback) = 0;
virtual HRESULT GetBufferedVideoFrameCount (/* out */ uint32_t* bufferedFrameCount) = 0;
/* Audio Output */
virtual HRESULT EnableAudioOutput (/* in */ BMDAudioSampleRate sampleRate, /* in */ BMDAudioSampleType sampleType, /* in */ uint32_t channelCount, /* in */ BMDAudioOutputStreamType streamType) = 0;
virtual HRESULT DisableAudioOutput (void) = 0;
virtual HRESULT WriteAudioSamplesSync (/* in */ void* buffer, /* in */ uint32_t sampleFrameCount, /* out */ uint32_t* sampleFramesWritten) = 0;
virtual HRESULT BeginAudioPreroll (void) = 0;
virtual HRESULT EndAudioPreroll (void) = 0;
virtual HRESULT ScheduleAudioSamples (/* in */ void* buffer, /* in */ uint32_t sampleFrameCount, /* in */ BMDTimeValue streamTime, /* in */ BMDTimeScale timeScale, /* out */ uint32_t* sampleFramesWritten) = 0;
virtual HRESULT GetBufferedAudioSampleFrameCount (/* out */ uint32_t* bufferedSampleFrameCount) = 0;
virtual HRESULT FlushBufferedAudioSamples (void) = 0;
virtual HRESULT SetAudioCallback (/* in */ IDeckLinkAudioOutputCallback* theCallback) = 0;
/* Output Control */
virtual HRESULT StartScheduledPlayback (/* in */ BMDTimeValue playbackStartTime, /* in */ BMDTimeScale timeScale, /* in */ double playbackSpeed) = 0;
virtual HRESULT StopScheduledPlayback (/* in */ BMDTimeValue stopPlaybackAtTime, /* out */ BMDTimeValue* actualStopTime, /* in */ BMDTimeScale timeScale) = 0;
virtual HRESULT IsScheduledPlaybackRunning (/* out */ bool* active) = 0;
virtual HRESULT GetScheduledStreamTime (/* in */ BMDTimeScale desiredTimeScale, /* out */ BMDTimeValue* streamTime, /* out */ double* playbackSpeed) = 0;
virtual HRESULT GetReferenceStatus (/* out */ BMDReferenceStatus* referenceStatus) = 0;
/* Hardware Timing */
virtual HRESULT GetHardwareReferenceClock (/* in */ BMDTimeScale desiredTimeScale, /* out */ BMDTimeValue* hardwareTime, /* out */ BMDTimeValue* timeInFrame, /* out */ BMDTimeValue* ticksPerFrame) = 0;
virtual HRESULT GetFrameCompletionReferenceTimestamp (/* in */ IDeckLinkVideoFrame_v14_2_1* theFrame, /* in */ BMDTimeScale desiredTimeScale, /* out */ BMDTimeValue* frameCompletionTimestamp) = 0;
protected:
virtual ~IDeckLinkOutput_v14_2_1 () {} // call Release method to drop reference count
};
#endif

View file

@ -0,0 +1,103 @@
/* -LICENSE-START-
** Copyright (c) 2025 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation (the
** "Software") to use, reproduce, display, distribute, sub-license, execute,
** and transmit the Software, and to prepare derivative works of the Software,
** and to permit third-parties to whom the Software is furnished to do so, in
** accordance with:
**
** (1) if the Software is obtained from Blackmagic Design, the End User License
** Agreement for the Software Development Kit (EULA) available at
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
**
** (2) if the Software is obtained from any third party, such licensing terms
** as notified by that third party,
**
** and all subject to the following:
**
** (3) the copyright notices in the Software and this entire statement,
** including the above license grant, this restriction and the following
** disclaimer, must be included in all copies of the Software, in whole or in
** part, and all derivative works of the Software, unless such copies or
** derivative works are solely in the form of machine-executable object code
** generated by a source language processor.
**
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
**
** A copy of the Software is available free of charge at
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
**
** -LICENSE-END-
*/
#pragma once
#include "DeckLinkAPI_v15_3_1.h"
// Type Declarations
BMD_CONST REFIID IID_IDeckLinkOutput_v15_3_1 = /* 1A8077F1-9FE2-4533-8147-2294305E253F */ { 0x1A,0x80,0x77,0xF1,0x9F,0xE2,0x45,0x33,0x81,0x47,0x22,0x94,0x30,0x5E,0x25,0x3F };
#if defined(__cplusplus)
/* Interface IDeckLinkOutput - Created by QueryInterface from IDeckLink. */
class BMD_PUBLIC IDeckLinkOutput_v15_3_1 : public IUnknown
{
public:
virtual HRESULT DoesSupportVideoMode (/* in */ BMDVideoConnection connection /* If a value of bmdVideoConnectionUnspecified is specified, the caller does not care about the connection */, /* in */ BMDDisplayMode requestedMode, /* in */ BMDPixelFormat requestedPixelFormat, /* in */ BMDVideoOutputConversionMode conversionMode, /* in */ BMDSupportedVideoModeFlags flags, /* out */ BMDDisplayMode* actualMode, /* out */ bool* supported) = 0;
virtual HRESULT GetDisplayMode (/* in */ BMDDisplayMode displayMode, /* out */ IDeckLinkDisplayMode** resultDisplayMode) = 0;
virtual HRESULT GetDisplayModeIterator (/* out */ IDeckLinkDisplayModeIterator** iterator) = 0;
virtual HRESULT SetScreenPreviewCallback (/* in */ IDeckLinkScreenPreviewCallback* previewCallback) = 0;
/* Video Output */
virtual HRESULT EnableVideoOutput (/* in */ BMDDisplayMode displayMode, /* in */ BMDVideoOutputFlags flags) = 0;
virtual HRESULT DisableVideoOutput (void) = 0;
virtual HRESULT CreateVideoFrame (/* in */ int32_t width, /* in */ int32_t height, /* in */ int32_t rowBytes, /* in */ BMDPixelFormat pixelFormat, /* in */ BMDFrameFlags flags, /* out */ IDeckLinkMutableVideoFrame** outFrame) = 0;
virtual HRESULT CreateVideoFrameWithBuffer (/* in */ int32_t width, /* in */ int32_t height, /* in */ int32_t rowBytes, /* in */ BMDPixelFormat pixelFormat, /* in */ BMDFrameFlags flags, /* in */ IDeckLinkVideoBuffer_v15_3_1* buffer, /* out */ IDeckLinkMutableVideoFrame** outFrame) = 0;
virtual HRESULT RowBytesForPixelFormat (/* in */ BMDPixelFormat pixelFormat, /* in */ int32_t width, /* out */ int32_t* rowBytes) = 0;
virtual HRESULT CreateAncillaryData (/* in */ BMDPixelFormat pixelFormat, /* out */ IDeckLinkVideoFrameAncillary** outBuffer) = 0; // Use of IDeckLinkVideoFrameAncillaryPackets is preferred
virtual HRESULT DisplayVideoFrameSync (/* in */ IDeckLinkVideoFrame* theFrame) = 0;
virtual HRESULT ScheduleVideoFrame (/* in */ IDeckLinkVideoFrame* theFrame, /* in */ BMDTimeValue displayTime, /* in */ BMDTimeValue displayDuration, /* in */ BMDTimeScale timeScale) = 0;
virtual HRESULT SetScheduledFrameCompletionCallback (/* in */ IDeckLinkVideoOutputCallback* theCallback) = 0;
virtual HRESULT GetBufferedVideoFrameCount (/* out */ uint32_t* bufferedFrameCount) = 0;
/* Audio Output */
virtual HRESULT EnableAudioOutput (/* in */ BMDAudioSampleRate sampleRate, /* in */ BMDAudioSampleType sampleType, /* in */ uint32_t channelCount, /* in */ BMDAudioOutputStreamType streamType) = 0;
virtual HRESULT DisableAudioOutput (void) = 0;
virtual HRESULT WriteAudioSamplesSync (/* in */ void* buffer, /* in */ uint32_t sampleFrameCount, /* out */ uint32_t* sampleFramesWritten) = 0;
virtual HRESULT BeginAudioPreroll (void) = 0;
virtual HRESULT EndAudioPreroll (void) = 0;
virtual HRESULT ScheduleAudioSamples (/* in */ void* buffer, /* in */ uint32_t sampleFrameCount, /* in */ BMDTimeValue streamTime, /* in */ BMDTimeScale timeScale, /* out */ uint32_t* sampleFramesWritten) = 0;
virtual HRESULT GetBufferedAudioSampleFrameCount (/* out */ uint32_t* bufferedSampleFrameCount) = 0;
virtual HRESULT FlushBufferedAudioSamples (void) = 0;
virtual HRESULT SetAudioCallback (/* in */ IDeckLinkAudioOutputCallback* theCallback) = 0;
/* Output Control */
virtual HRESULT StartScheduledPlayback (/* in */ BMDTimeValue playbackStartTime, /* in */ BMDTimeScale timeScale, /* in */ double playbackSpeed) = 0;
virtual HRESULT StopScheduledPlayback (/* in */ BMDTimeValue stopPlaybackAtTime, /* out */ BMDTimeValue* actualStopTime, /* in */ BMDTimeScale timeScale) = 0;
virtual HRESULT IsScheduledPlaybackRunning (/* out */ bool* active) = 0;
virtual HRESULT GetScheduledStreamTime (/* in */ BMDTimeScale desiredTimeScale, /* out */ BMDTimeValue* streamTime, /* out */ double* playbackSpeed) = 0;
virtual HRESULT GetReferenceStatus (/* out */ BMDReferenceStatus* referenceStatus) = 0;
/* Hardware Timing */
virtual HRESULT GetHardwareReferenceClock (/* in */ BMDTimeScale desiredTimeScale, /* out */ BMDTimeValue* hardwareTime, /* out */ BMDTimeValue* timeInFrame, /* out */ BMDTimeValue* ticksPerFrame) = 0;
virtual HRESULT GetFrameCompletionReferenceTimestamp (/* in */ IDeckLinkVideoFrame* theFrame, /* in */ BMDTimeScale desiredTimeScale, /* out */ BMDTimeValue* frameCompletionTimestamp) = 0;
protected:
virtual ~IDeckLinkOutput_v15_3_1 () {} // call Release method to drop reference count
};
#endif // defined(__cplusplus)

View file

@ -0,0 +1,134 @@
/* -LICENSE-START-
** Copyright (c) 2018 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation (the
** "Software") to use, reproduce, display, distribute, sub-license, execute,
** and transmit the Software, and to prepare derivative works of the Software,
** and to permit third-parties to whom the Software is furnished to do so, in
** accordance with:
**
** (1) if the Software is obtained from Blackmagic Design, the End User License
** Agreement for the Software Development Kit ("EULA") available at
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
**
** (2) if the Software is obtained from any third party, such licensing terms
** as notified by that third party,
**
** and all subject to the following:
**
** (3) the copyright notices in the Software and this entire statement,
** including the above license grant, this restriction and the following
** disclaimer, must be included in all copies of the Software, in whole or in
** part, and all derivative works of the Software, unless such copies or
** derivative works are solely in the form of machine-executable object code
** generated by a source language processor.
**
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
**
** A copy of the Software is available free of charge at
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
**
** -LICENSE-END-
*/
#ifndef BMD_DECKLINKAPI_v10_11_H
#define BMD_DECKLINKAPI_v10_11_H
#include "DeckLinkAPI.h"
// Interface ID Declarations
BMD_CONST REFIID IID_IDeckLinkAttributes_v10_11 = /* ABC11843-D966-44CB-96E2-A1CB5D3135C4 */ {0xAB,0xC1,0x18,0x43,0xD9,0x66,0x44,0xCB,0x96,0xE2,0xA1,0xCB,0x5D,0x31,0x35,0xC4};
BMD_CONST REFIID IID_IDeckLinkNotification_v10_11 = /* 0A1FB207-E215-441B-9B19-6FA1575946C5 */ {0x0A,0x1F,0xB2,0x07,0xE2,0x15,0x44,0x1B,0x9B,0x19,0x6F,0xA1,0x57,0x59,0x46,0xC5};
/* Enum BMDDisplayModeSupport_v10_11 - Output mode supported flags */
typedef uint32_t BMDDisplayModeSupport_v10_11;
enum _BMDDisplayModeSupport_v10_11 {
bmdDisplayModeNotSupported_v10_11 = 0,
bmdDisplayModeSupported_v10_11,
bmdDisplayModeSupportedWithConversion_v10_11
};
/* Enum BMDDuplexMode_v10_11 - Duplex for configurable ports */
typedef uint32_t BMDDuplexMode_v10_11;
enum _BMDDuplexMode_v10_11 {
bmdDuplexModeFull_v10_11 = /* 'fdup' */ 0x66647570,
bmdDuplexModeHalf_v10_11 = /* 'hdup' */ 0x68647570
};
/* Enum BMDDeckLinkAttributeID_v10_11 - DeckLink Attribute ID */
enum _BMDDeckLinkAttributeID_v10_11 {
/* Flags */
BMDDeckLinkSupportsDuplexModeConfiguration_v10_11 = 'dupx',
BMDDeckLinkSupportsHDKeying_v10_11 = 'keyh',
/* Integers */
BMDDeckLinkPairedDevicePersistentID_v10_11 = 'ppid',
BMDDeckLinkSupportsFullDuplex_v10_11 = 'fdup',
};
enum _BMDDeckLinkStatusID_v10_11 {
bmdDeckLinkStatusDuplexMode_v10_11 = 'dupx',
};
typedef uint32_t BMDDuplexStatus_v10_11;
enum _BMDDuplexStatus_v10_11 {
bmdDuplexFullDuplex_v10_11 = 'fdup',
bmdDuplexHalfDuplex_v10_11 = 'hdup',
bmdDuplexSimplex_v10_11 = 'splx',
bmdDuplexInactive_v10_11 = 'inac',
};
#if defined(__cplusplus)
/* Interface IDeckLinkAttributes_v10_11 - DeckLink Attribute interface */
class BMD_PUBLIC IDeckLinkAttributes_v10_11 : public IUnknown
{
public:
virtual HRESULT GetFlag (/* in */ BMDDeckLinkAttributeID cfgID, /* out */ bool *value) = 0;
virtual HRESULT GetInt (/* in */ BMDDeckLinkAttributeID cfgID, /* out */ int64_t *value) = 0;
virtual HRESULT GetFloat (/* in */ BMDDeckLinkAttributeID cfgID, /* out */ double *value) = 0;
virtual HRESULT GetString (/* in */ BMDDeckLinkAttributeID cfgID, /* out */ const char **value) = 0;
protected:
virtual ~IDeckLinkAttributes_v10_11 () {} // call Release method to drop reference count
};
/* Interface IDeckLinkNotification_v10_11 - DeckLink Notification interface */
class BMD_PUBLIC IDeckLinkNotification_v10_11 : public IUnknown
{
public:
virtual HRESULT Subscribe (/* in */ BMDNotifications topic, /* in */ IDeckLinkNotificationCallback *theCallback) = 0;
virtual HRESULT Unsubscribe (/* in */ BMDNotifications topic, /* in */ IDeckLinkNotificationCallback *theCallback) = 0;
};
/* Functions */
extern "C" {
BMD_PUBLIC IDeckLinkIterator* CreateDeckLinkIteratorInstance_v10_11 (void);
BMD_PUBLIC IDeckLinkDiscovery* CreateDeckLinkDiscoveryInstance_v10_11 (void);
BMD_PUBLIC IDeckLinkAPIInformation* CreateDeckLinkAPIInformationInstance_v10_11 (void);
BMD_PUBLIC IDeckLinkGLScreenPreviewHelper* CreateOpenGLScreenPreviewHelper_v10_11 (void);
BMD_PUBLIC IDeckLinkVideoConversion* CreateVideoConversionInstance_v10_11 (void);
BMD_PUBLIC IDeckLinkVideoFrameAncillaryPackets* CreateVideoFrameAncillaryPacketsInstance_v10_11 (void); // For use when creating a custom IDeckLinkVideoFrame without wrapping IDeckLinkOutput::CreateVideoFrame
}
#endif // defined(__cplusplus)
#endif /* defined(BMD_DECKLINKAPI_v10_11_H) */

View file

@ -0,0 +1,68 @@
/* -LICENSE-START-
** Copyright (c) 2014 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation (the
** "Software") to use, reproduce, display, distribute, sub-license, execute,
** and transmit the Software, and to prepare derivative works of the Software,
** and to permit third-parties to whom the Software is furnished to do so, in
** accordance with:
**
** (1) if the Software is obtained from Blackmagic Design, the End User License
** Agreement for the Software Development Kit ("EULA") available at
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
**
** (2) if the Software is obtained from any third party, such licensing terms
** as notified by that third party,
**
** and all subject to the following:
**
** (3) the copyright notices in the Software and this entire statement,
** including the above license grant, this restriction and the following
** disclaimer, must be included in all copies of the Software, in whole or in
** part, and all derivative works of the Software, unless such copies or
** derivative works are solely in the form of machine-executable object code
** generated by a source language processor.
**
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
**
** A copy of the Software is available free of charge at
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
**
** -LICENSE-END-
*/
#ifndef BMD_DECKLINKAPI_v10_2_H
#define BMD_DECKLINKAPI_v10_2_H
#include "DeckLinkAPI.h"
// Type Declarations
/* Enum BMDDeckLinkConfigurationID - DeckLink Configuration ID */
typedef uint32_t BMDDeckLinkConfigurationID_v10_2;
enum _BMDDeckLinkConfigurationID_v10_2 {
/* Video output flags */
bmdDeckLinkConfig3GBpsVideoOutput_v10_2 = '3gbs',
};
/* Enum BMDAudioConnection_v10_2 - Audio connection types */
typedef uint32_t BMDAudioConnection_v10_2;
enum _BMDAudioConnection_v10_2 {
bmdAudioConnectionEmbedded_v10_2 = /* 'embd' */ 0x656D6264,
bmdAudioConnectionAESEBU_v10_2 = /* 'aes ' */ 0x61657320,
bmdAudioConnectionAnalog_v10_2 = /* 'anlg' */ 0x616E6C67,
bmdAudioConnectionAnalogXLR_v10_2 = /* 'axlr' */ 0x61786C72,
bmdAudioConnectionAnalogRCA_v10_2 = /* 'arca' */ 0x61726361
};
#endif /* defined(BMD_DECKLINKAPI_v10_2_H) */

View file

@ -0,0 +1,58 @@
/* -LICENSE-START-
** Copyright (c) 2015 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation (the
** "Software") to use, reproduce, display, distribute, sub-license, execute,
** and transmit the Software, and to prepare derivative works of the Software,
** and to permit third-parties to whom the Software is furnished to do so, in
** accordance with:
**
** (1) if the Software is obtained from Blackmagic Design, the End User License
** Agreement for the Software Development Kit ("EULA") available at
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
**
** (2) if the Software is obtained from any third party, such licensing terms
** as notified by that third party,
**
** and all subject to the following:
**
** (3) the copyright notices in the Software and this entire statement,
** including the above license grant, this restriction and the following
** disclaimer, must be included in all copies of the Software, in whole or in
** part, and all derivative works of the Software, unless such copies or
** derivative works are solely in the form of machine-executable object code
** generated by a source language processor.
**
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
**
** A copy of the Software is available free of charge at
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
**
** -LICENSE-END-
*/
#ifndef BMD_DECKLINKAPI_v10_4_H
#define BMD_DECKLINKAPI_v10_4_H
#include "DeckLinkAPI.h"
// Type Declarations
/* Enum BMDDeckLinkConfigurationID - DeckLink Configuration ID */
typedef uint32_t BMDDeckLinkConfigurationID_v10_4;
enum _BMDDeckLinkConfigurationID_v10_4 {
/* Video output flags */
bmdDeckLinkConfigSingleLinkVideoOutput_v10_4 = /* 'sglo' */ 0x73676C6F,
};
#endif /* defined(BMD_DECKLINKAPI_v10_4_H) */

View file

@ -0,0 +1,59 @@
/* -LICENSE-START-
** Copyright (c) 2015 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation (the
** "Software") to use, reproduce, display, distribute, sub-license, execute,
** and transmit the Software, and to prepare derivative works of the Software,
** and to permit third-parties to whom the Software is furnished to do so, in
** accordance with:
**
** (1) if the Software is obtained from Blackmagic Design, the End User License
** Agreement for the Software Development Kit ("EULA") available at
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
**
** (2) if the Software is obtained from any third party, such licensing terms
** as notified by that third party,
**
** and all subject to the following:
**
** (3) the copyright notices in the Software and this entire statement,
** including the above license grant, this restriction and the following
** disclaimer, must be included in all copies of the Software, in whole or in
** part, and all derivative works of the Software, unless such copies or
** derivative works are solely in the form of machine-executable object code
** generated by a source language processor.
**
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
**
** A copy of the Software is available free of charge at
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
**
** -LICENSE-END-
*/
#ifndef BMD_DECKLINKAPI_v10_5_H
#define BMD_DECKLINKAPI_v10_5_H
#include "DeckLinkAPI.h"
// Type Declarations
/* Enum BMDDeckLinkAttributeID - DeckLink Attribute ID */
typedef uint32_t BMDDeckLinkAttributeID_v10_5;
enum _BMDDeckLinkAttributeID_v10_5 {
/* Integers */
BMDDeckLinkDeviceBusyState_v10_5 = /* 'dbst' */ 0x64627374,
};
#endif /* defined(BMD_DECKLINKAPI_v10_5_H) */

View file

@ -0,0 +1,63 @@
/* -LICENSE-START-
** Copyright (c) 2016 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation (the
** "Software") to use, reproduce, display, distribute, sub-license, execute,
** and transmit the Software, and to prepare derivative works of the Software,
** and to permit third-parties to whom the Software is furnished to do so, in
** accordance with:
**
** (1) if the Software is obtained from Blackmagic Design, the End User License
** Agreement for the Software Development Kit ("EULA") available at
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
**
** (2) if the Software is obtained from any third party, such licensing terms
** as notified by that third party,
**
** and all subject to the following:
**
** (3) the copyright notices in the Software and this entire statement,
** including the above license grant, this restriction and the following
** disclaimer, must be included in all copies of the Software, in whole or in
** part, and all derivative works of the Software, unless such copies or
** derivative works are solely in the form of machine-executable object code
** generated by a source language processor.
**
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
**
** A copy of the Software is available free of charge at
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
**
** -LICENSE-END-
*/
#ifndef BMD_DECKLINKAPI_v10_6_H
#define BMD_DECKLINKAPI_v10_6_H
#include "DeckLinkAPI.h"
// Type Declarations
/* Enum BMDDeckLinkAttributeID - DeckLink Attribute ID */
typedef uint32_t BMDDeckLinkAttributeID_c10_6;
enum _BMDDeckLinkAttributeID_v10_6 {
/* Flags */
BMDDeckLinkSupportsDesktopDisplay_v10_6 = /* 'extd' */ 0x65787464,
};
typedef uint32_t BMDIdleVideoOutputOperation_v10_6;
enum _BMDIdleVideoOutputOperation_v10_6 {
bmdIdleVideoOutputDesktop_v10_6 = /* 'desk' */ 0x6465736B
};
#endif /* defined(BMD_DECKLINKAPI_v10_6_H) */

View file

@ -0,0 +1,58 @@
/* -LICENSE-START-
** Copyright (c) 2017 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation (the
** "Software") to use, reproduce, display, distribute, sub-license, execute,
** and transmit the Software, and to prepare derivative works of the Software,
** and to permit third-parties to whom the Software is furnished to do so, in
** accordance with:
**
** (1) if the Software is obtained from Blackmagic Design, the End User License
** Agreement for the Software Development Kit ("EULA") available at
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
**
** (2) if the Software is obtained from any third party, such licensing terms
** as notified by that third party,
**
** and all subject to the following:
**
** (3) the copyright notices in the Software and this entire statement,
** including the above license grant, this restriction and the following
** disclaimer, must be included in all copies of the Software, in whole or in
** part, and all derivative works of the Software, unless such copies or
** derivative works are solely in the form of machine-executable object code
** generated by a source language processor.
**
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
**
** A copy of the Software is available free of charge at
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
**
** -LICENSE-END-
*/
#ifndef BMD_DECKLINKAPI_v10_9_H
#define BMD_DECKLINKAPI_v10_9_H
#include "DeckLinkAPI.h"
// Type Declarations
/* Enum BMDDeckLinkAttributeID - DeckLink Attribute ID */
typedef uint32_t BMDDeckLinkConfigurationID_v10_9;
enum _BMDDeckLinkConfigurationID_v10_9 {
/* Flags */
bmdDeckLinkConfig1080pNotPsF_v10_9 = 'fpro',
};
#endif /* defined(BMD_DECKLINKAPI_v10_9_H) */

View file

@ -0,0 +1,113 @@
/* -LICENSE-START-
** Copyright (c) 2020 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation (the
** "Software") to use, reproduce, display, distribute, sub-license, execute,
** and transmit the Software, and to prepare derivative works of the Software,
** and to permit third-parties to whom the Software is furnished to do so, in
** accordance with:
**
** (1) if the Software is obtained from Blackmagic Design, the End User License
** Agreement for the Software Development Kit ("EULA") available at
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
**
** (2) if the Software is obtained from any third party, such licensing terms
** as notified by that third party,
**
** and all subject to the following:
**
** (3) the copyright notices in the Software and this entire statement,
** including the above license grant, this restriction and the following
** disclaimer, must be included in all copies of the Software, in whole or in
** part, and all derivative works of the Software, unless such copies or
** derivative works are solely in the form of machine-executable object code
** generated by a source language processor.
**
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
**
** A copy of the Software is available free of charge at
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
**
** -LICENSE-END-
*/
#ifndef BMD_DECKLINKAPI_v11_5_H
#define BMD_DECKLINKAPI_v11_5_H
#include "DeckLinkAPI.h"
BMD_CONST REFIID IID_IDeckLinkVideoFrameMetadataExtensions_v11_5 = /* D5973DC9-6432-46D0-8F0B-2496F8A1238F */ {0xD5,0x97,0x3D,0xC9,0x64,0x32,0x46,0xD0,0x8F,0x0B,0x24,0x96,0xF8,0xA1,0x23,0x8F};
/* Enum BMDDeckLinkFrameMetadataID - DeckLink Frame Metadata ID */
typedef uint32_t BMDDeckLinkFrameMetadataID_v11_5;
enum _BMDDeckLinkFrameMetadataID_v11_5 {
bmdDeckLinkFrameMetadataCintelFilmType_v11_5 = /* 'cfty' */ 0x63667479, // Current film type
bmdDeckLinkFrameMetadataCintelFilmGauge_v11_5 = /* 'cfga' */ 0x63666761, // Current film gauge
bmdDeckLinkFrameMetadataCintelKeykodeLow_v11_5 = /* 'ckkl' */ 0x636B6B6C, // Raw keykode value - low 64 bits
bmdDeckLinkFrameMetadataCintelKeykodeHigh_v11_5 = /* 'ckkh' */ 0x636B6B68, // Raw keykode value - high 64 bits
bmdDeckLinkFrameMetadataCintelTile1Size_v11_5 = /* 'ct1s' */ 0x63743173, // Size in bytes of compressed raw tile 1
bmdDeckLinkFrameMetadataCintelTile2Size_v11_5 = /* 'ct2s' */ 0x63743273, // Size in bytes of compressed raw tile 2
bmdDeckLinkFrameMetadataCintelTile3Size_v11_5 = /* 'ct3s' */ 0x63743373, // Size in bytes of compressed raw tile 3
bmdDeckLinkFrameMetadataCintelTile4Size_v11_5 = /* 'ct4s' */ 0x63743473, // Size in bytes of compressed raw tile 4
bmdDeckLinkFrameMetadataCintelImageWidth_v11_5 = /* 'IWPx' */ 0x49575078, // Width in pixels of image
bmdDeckLinkFrameMetadataCintelImageHeight_v11_5 = /* 'IHPx' */ 0x49485078, // Height in pixels of image
bmdDeckLinkFrameMetadataCintelLinearMaskingRedInRed_v11_5 = /* 'mrir' */ 0x6D726972, // Red in red linear masking parameter
bmdDeckLinkFrameMetadataCintelLinearMaskingGreenInRed_v11_5 = /* 'mgir' */ 0x6D676972, // Green in red linear masking parameter
bmdDeckLinkFrameMetadataCintelLinearMaskingBlueInRed_v11_5 = /* 'mbir' */ 0x6D626972, // Blue in red linear masking parameter
bmdDeckLinkFrameMetadataCintelLinearMaskingRedInGreen_v11_5 = /* 'mrig' */ 0x6D726967, // Red in green linear masking parameter
bmdDeckLinkFrameMetadataCintelLinearMaskingGreenInGreen_v11_5 = /* 'mgig' */ 0x6D676967, // Green in green linear masking parameter
bmdDeckLinkFrameMetadataCintelLinearMaskingBlueInGreen_v11_5 = /* 'mbig' */ 0x6D626967, // Blue in green linear masking parameter
bmdDeckLinkFrameMetadataCintelLinearMaskingRedInBlue_v11_5 = /* 'mrib' */ 0x6D726962, // Red in blue linear masking parameter
bmdDeckLinkFrameMetadataCintelLinearMaskingGreenInBlue_v11_5 = /* 'mgib' */ 0x6D676962, // Green in blue linear masking parameter
bmdDeckLinkFrameMetadataCintelLinearMaskingBlueInBlue_v11_5 = /* 'mbib' */ 0x6D626962, // Blue in blue linear masking parameter
bmdDeckLinkFrameMetadataCintelLogMaskingRedInRed_v11_5 = /* 'mlrr' */ 0x6D6C7272, // Red in red log masking parameter
bmdDeckLinkFrameMetadataCintelLogMaskingGreenInRed_v11_5 = /* 'mlgr' */ 0x6D6C6772, // Green in red log masking parameter
bmdDeckLinkFrameMetadataCintelLogMaskingBlueInRed_v11_5 = /* 'mlbr' */ 0x6D6C6272, // Blue in red log masking parameter
bmdDeckLinkFrameMetadataCintelLogMaskingRedInGreen_v11_5 = /* 'mlrg' */ 0x6D6C7267, // Red in green log masking parameter
bmdDeckLinkFrameMetadataCintelLogMaskingGreenInGreen_v11_5 = /* 'mlgg' */ 0x6D6C6767, // Green in green log masking parameter
bmdDeckLinkFrameMetadataCintelLogMaskingBlueInGreen_v11_5 = /* 'mlbg' */ 0x6D6C6267, // Blue in green log masking parameter
bmdDeckLinkFrameMetadataCintelLogMaskingRedInBlue_v11_5 = /* 'mlrb' */ 0x6D6C7262, // Red in blue log masking parameter
bmdDeckLinkFrameMetadataCintelLogMaskingGreenInBlue_v11_5 = /* 'mlgb' */ 0x6D6C6762, // Green in blue log masking parameter
bmdDeckLinkFrameMetadataCintelLogMaskingBlueInBlue_v11_5 = /* 'mlbb' */ 0x6D6C6262, // Blue in blue log masking parameter
bmdDeckLinkFrameMetadataCintelFilmFrameRate_v11_5 = /* 'cffr' */ 0x63666672, // Film frame rate
bmdDeckLinkFrameMetadataCintelOffsetToApplyHorizontal_v11_5 = /* 'otah' */ 0x6F746168, // Horizontal offset (pixels) to be applied to image
bmdDeckLinkFrameMetadataCintelOffsetToApplyVertical_v11_5 = /* 'otav' */ 0x6F746176, // Vertical offset (pixels) to be applied to image
bmdDeckLinkFrameMetadataCintelGainRed_v11_5 = /* 'LfRd' */ 0x4C665264, // Red gain parameter to apply after log
bmdDeckLinkFrameMetadataCintelGainGreen_v11_5 = /* 'LfGr' */ 0x4C664772, // Green gain parameter to apply after log
bmdDeckLinkFrameMetadataCintelGainBlue_v11_5 = /* 'LfBl' */ 0x4C66426C, // Blue gain parameter to apply after log
bmdDeckLinkFrameMetadataCintelLiftRed_v11_5 = /* 'GnRd' */ 0x476E5264, // Red lift parameter to apply after log and gain
bmdDeckLinkFrameMetadataCintelLiftGreen_v11_5 = /* 'GnGr' */ 0x476E4772, // Green lift parameter to apply after log and gain
bmdDeckLinkFrameMetadataCintelLiftBlue_v11_5 = /* 'GnBl' */ 0x476E426C, // Blue lift parameter to apply after log and gain
bmdDeckLinkFrameMetadataCintelHDRGainRed_v11_5 = /* 'HGRd' */ 0x48475264, // Red gain parameter to apply to linear data for HDR Combination
bmdDeckLinkFrameMetadataCintelHDRGainGreen_v11_5 = /* 'HGGr' */ 0x48474772, // Green gain parameter to apply to linear data for HDR Combination
bmdDeckLinkFrameMetadataCintelHDRGainBlue_v11_5 = /* 'HGBl' */ 0x4847426C, // Blue gain parameter to apply to linear data for HDR Combination
bmdDeckLinkFrameMetadataCintel16mmCropRequired_v11_5 = /* 'c16c' */ 0x63313663, // The image should be cropped to 16mm size
bmdDeckLinkFrameMetadataCintelInversionRequired_v11_5 = /* 'cinv' */ 0x63696E76, // The image should be colour inverted
bmdDeckLinkFrameMetadataCintelFlipRequired_v11_5 = /* 'cflr' */ 0x63666C72, // The image should be flipped horizontally
bmdDeckLinkFrameMetadataCintelFocusAssistEnabled_v11_5 = /* 'cfae' */ 0x63666165, // Focus Assist is currently enabled
bmdDeckLinkFrameMetadataCintelKeykodeIsInterpolated_v11_5 = /* 'kkii' */ 0x6B6B6969 // The keykode for this frame is interpolated from nearby keykodes
};
/* Interface IDeckLinkVideoFrameMetadataExtensions - Optional interface implemented on IDeckLinkVideoFrame to support frame metadata such as HDMI HDR information */
class BMD_PUBLIC IDeckLinkVideoFrameMetadataExtensions_v11_5 : public IUnknown
{
public:
virtual HRESULT GetInt (/* in */ BMDDeckLinkFrameMetadataID_v11_5 metadataID, /* out */ int64_t *value) = 0;
virtual HRESULT GetFloat (/* in */ BMDDeckLinkFrameMetadataID_v11_5 metadataID, /* out */ double *value) = 0;
virtual HRESULT GetFlag (/* in */ BMDDeckLinkFrameMetadataID_v11_5 metadataID, /* out */ bool* value) = 0;
virtual HRESULT GetString (/* in */ BMDDeckLinkFrameMetadataID_v11_5 metadataID, /* out */ const char **value) = 0;
protected:
virtual ~IDeckLinkVideoFrameMetadataExtensions_v11_5 () {} // call Release method to drop reference count
};
#endif /* defined(BMD_DECKLINKAPI_v11_5_H) */

View file

@ -0,0 +1,57 @@
/* -LICENSE-START-
** Copyright (c) 2020 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation (the
** "Software") to use, reproduce, display, distribute, sub-license, execute,
** and transmit the Software, and to prepare derivative works of the Software,
** and to permit third-parties to whom the Software is furnished to do so, in
** accordance with:
**
** (1) if the Software is obtained from Blackmagic Design, the End User License
** Agreement for the Software Development Kit ("EULA") available at
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
**
** (2) if the Software is obtained from any third party, such licensing terms
** as notified by that third party,
**
** and all subject to the following:
**
** (3) the copyright notices in the Software and this entire statement,
** including the above license grant, this restriction and the following
** disclaimer, must be included in all copies of the Software, in whole or in
** part, and all derivative works of the Software, unless such copies or
** derivative works are solely in the form of machine-executable object code
** generated by a source language processor.
**
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
**
** A copy of the Software is available free of charge at
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
**
** -LICENSE-END-
*/
#ifndef BMD_DECKLINKAPI_v11_5_1_H
#define BMD_DECKLINKAPI_v11_5_1_H
#include "DeckLinkAPI.h"
/* Enum BMDDeckLinkStatusID - DeckLink Status ID */
typedef uint32_t BMDDeckLinkStatusID_v11_5_1;
enum _BMDDeckLinkStatusID_v11_5_1 {
/* Video output flags */
bmdDeckLinkStatusDetectedVideoInputFlags_v11_5_1 = /* 'dvif' */ 0x64766966,
};
#endif /* defined(BMD_DECKLINKAPI_v11_5_1_H) */

View file

@ -0,0 +1,110 @@
/* -LICENSE-START-
** Copyright (c) 2018 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation (the
** "Software") to use, reproduce, display, distribute, sub-license, execute,
** and transmit the Software, and to prepare derivative works of the Software,
** and to permit third-parties to whom the Software is furnished to do so, in
** accordance with:
**
** (1) if the Software is obtained from Blackmagic Design, the End User License
** Agreement for the Software Development Kit (EULA) available at
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
**
** (2) if the Software is obtained from any third party, such licensing terms
** as notified by that third party,
**
** and all subject to the following:
**
** (3) the copyright notices in the Software and this entire statement,
** including the above license grant, this restriction and the following
** disclaimer, must be included in all copies of the Software, in whole or in
** part, and all derivative works of the Software, unless such copies or
** derivative works are solely in the form of machine-executable object code
** generated by a source language processor.
**
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
**
** A copy of the Software is available free of charge at
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
**
** -LICENSE-END-
*/
#ifndef BMD_DECKLINKAPI_v14_2_1_H
#define BMD_DECKLINKAPI_v14_2_1_H
#include "DeckLinkAPI.h"
#include "DeckLinkAPIVideoConversion_v14_2_1.h"
#include "DeckLinkAPIGLScreenPreview_v14_2_1.h"
#include "DeckLinkAPIMetalScreenPreview_v14_2_1.h"
#include "DeckLinkAPIScreenPreviewCallback_v14_2_1.h"
#include "DeckLinkAPIVideoOutput_v14_2_1.h"
#include "DeckLinkAPIVideoInput_v14_2_1.h"
#include "DeckLinkAPIVideoFrame3DExtensions_v14_2_1.h"
// Interface ID Declarations
BMD_CONST REFIID IID_IDeckLinkEncoderInput_v14_2_1 = /* F222551D-13DF-4FD8-B587-9D4F19EC12C9 */ { 0xF2,0x22,0x55,0x1D,0x13,0xDF,0x4F,0xD8,0xB5,0x87,0x9D,0x4F,0x19,0xEC,0x12,0xC9 };
#if defined(__cplusplus)
/* Interface IDeckLinkEncoderInput - Created by QueryInterface from IDeckLink. */
class BMD_PUBLIC IDeckLinkEncoderInput_v14_2_1 : public IUnknown
{
public:
virtual HRESULT DoesSupportVideoMode (/* in */ BMDVideoConnection connection /* If a value of bmdVideoConnectionUnspecified is specified, the caller does not care about the connection */, /* in */ BMDDisplayMode requestedMode, /* in */ BMDPixelFormat requestedCodec, /* in */ uint32_t requestedCodecProfile, /* in */ BMDSupportedVideoModeFlags flags, /* out */ bool* supported) = 0;
virtual HRESULT GetDisplayMode (/* in */ BMDDisplayMode displayMode, /* out */ IDeckLinkDisplayMode** resultDisplayMode) = 0;
virtual HRESULT GetDisplayModeIterator (/* out */ IDeckLinkDisplayModeIterator** iterator) = 0;
/* Video Input */
virtual HRESULT EnableVideoInput (/* in */ BMDDisplayMode displayMode, /* in */ BMDPixelFormat pixelFormat, /* in */ BMDVideoInputFlags flags) = 0;
virtual HRESULT DisableVideoInput (void) = 0;
virtual HRESULT GetAvailablePacketsCount (/* out */ uint32_t* availablePacketsCount) = 0;
virtual HRESULT SetMemoryAllocator (/* in */ IDeckLinkMemoryAllocator_v14_2_1* theAllocator) = 0;
/* Audio Input */
virtual HRESULT EnableAudioInput (/* in */ BMDAudioFormat audioFormat, /* in */ BMDAudioSampleRate sampleRate, /* in */ BMDAudioSampleType sampleType, /* in */ uint32_t channelCount) = 0;
virtual HRESULT DisableAudioInput (void) = 0;
virtual HRESULT GetAvailableAudioSampleFrameCount (/* out */ uint32_t* availableSampleFrameCount) = 0;
/* Input Control */
virtual HRESULT StartStreams (void) = 0;
virtual HRESULT StopStreams (void) = 0;
virtual HRESULT PauseStreams (void) = 0;
virtual HRESULT FlushStreams (void) = 0;
virtual HRESULT SetCallback (/* in */ IDeckLinkEncoderInputCallback* theCallback) = 0;
/* Hardware Timing */
virtual HRESULT GetHardwareReferenceClock (/* in */ BMDTimeScale desiredTimeScale, /* out */ BMDTimeValue* hardwareTime, /* out */ BMDTimeValue* timeInFrame, /* out */ BMDTimeValue* ticksPerFrame) = 0;
protected:
virtual ~IDeckLinkEncoderInput_v14_2_1 () {} // call Release method to drop reference count
};
/* Functions */
extern "C" {
BMD_PUBLIC IDeckLinkIterator* CreateDeckLinkIteratorInstance_v14_2_1(void);
BMD_PUBLIC IDeckLinkDiscovery* CreateDeckLinkDiscoveryInstance_v14_2_1(void);
BMD_PUBLIC IDeckLinkAPIInformation* CreateDeckLinkAPIInformationInstance_v14_2_1(void);
BMD_PUBLIC IDeckLinkGLScreenPreviewHelper_v14_2_1* CreateOpenGLScreenPreviewHelper_v14_2_1(void);
BMD_PUBLIC IDeckLinkGLScreenPreviewHelper_v14_2_1* CreateOpenGL3ScreenPreviewHelper_v14_2_1(void); // Requires OpenGL 3.2 support and provides improved performance and color handling
BMD_PUBLIC IDeckLinkVideoConversion_v14_2_1* CreateVideoConversionInstance_v14_2_1(void);
BMD_PUBLIC IDeckLinkVideoFrameAncillaryPackets* CreateVideoFrameAncillaryPacketsInstance_v14_2_1(void); // For use when creating a custom IDeckLinkVideoFrame without wrapping IDeckLinkOutput::CreateVideoFrame
}
#endif // defined(__cplusplus)
#endif /* defined(BMD_DECKLINKAPI_v14_2_1_H) */

View file

@ -0,0 +1,96 @@
/* -LICENSE-START-
** Copyright (c) 2025 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation (the
** "Software") to use, reproduce, display, distribute, sub-license, execute,
** and transmit the Software, and to prepare derivative works of the Software,
** and to permit third-parties to whom the Software is furnished to do so, in
** accordance with:
**
** (1) if the Software is obtained from Blackmagic Design, the End User License
** Agreement for the Software Development Kit ("EULA") available at
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
**
** (2) if the Software is obtained from any third party, such licensing terms
** as notified by that third party,
**
** and all subject to the following:
**
** (3) the copyright notices in the Software and this entire statement,
** including the above license grant, this restriction and the following
** disclaimer, must be included in all copies of the Software, in whole or in
** part, and all derivative works of the Software, unless such copies or
** derivative works are solely in the form of machine-executable object code
** generated by a source language processor.
**
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
**
** A copy of the Software is available free of charge at
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
**
** -LICENSE-END-
*/
#ifndef BMD_DECKLINKAPI_v15_2_H
#define BMD_DECKLINKAPI_v15_2_H
#include "DeckLinkAPI.h"
// Interface ID Declarations
BMD_CONST REFIID IID_IDeckLinkAncillaryPacket_v15_2 = /* CC5BBF7E-029C-4D3B-9158-6000EF5E3670 */ { 0xCC,0x5B,0xBF,0x7E,0x02,0x9C,0x4D,0x3B,0x91,0x58,0x60,0x00,0xEF,0x5E,0x36,0x70 };
BMD_CONST REFIID IID_IDeckLinkAncillaryPacketIterator_v15_2 = /* 3FC8994B-88FB-4C17-968F-9AAB69D964A7 */ { 0x3F,0xC8,0x99,0x4B,0x88,0xFB,0x4C,0x17,0x96,0x8F,0x9A,0xAB,0x69,0xD9,0x64,0xA7 };
BMD_CONST REFIID IID_IDeckLinkVideoFrameAncillaryPackets_v15_2 = /* 6C186C0F-459E-41D8-AEE2-4812D81AEE68 */ { 0x6C,0x18,0x6C,0x0F,0x45,0x9E,0x41,0xD8,0xAE,0xE2,0x48,0x12,0xD8,0x1A,0xEE,0x68 };
/* Interface IDeckLinkAncillaryPacket - On output, user needs to implement this interface */
#if defined(__cplusplus)
class BMD_PUBLIC IDeckLinkAncillaryPacket_v15_2 : public IUnknown
{
public:
virtual HRESULT GetBytes (/* in */ BMDAncillaryPacketFormat format /* For output, only one format need be offered */, /* out */ const void** data /* Optional */, /* out */ uint32_t* size /* Optional */) = 0;
virtual uint8_t GetDID (void) = 0;
virtual uint8_t GetSDID (void) = 0;
virtual uint32_t GetLineNumber (void) = 0; // On output, zero is auto
virtual uint8_t GetDataStreamIndex (void) = 0; // Usually zero. Can only be 1 if non-SD and the first data stream is completely full
protected:
virtual ~IDeckLinkAncillaryPacket_v15_2 () {} // call Release method to drop reference count
};
/* Interface IDeckLinkAncillaryPacketIterator - Enumerates ancillary packets */
class BMD_PUBLIC IDeckLinkAncillaryPacketIterator_v15_2 : public IUnknown
{
public:
virtual HRESULT Next (/* out */ IDeckLinkAncillaryPacket_v15_2** packet) = 0;
protected:
virtual ~IDeckLinkAncillaryPacketIterator_v15_2 () {} // call Release method to drop reference count
};
/* Interface IDeckLinkVideoFrameAncillaryPackets - Obtained through QueryInterface on an IDeckLinkVideoFrame object. */
class BMD_PUBLIC IDeckLinkVideoFrameAncillaryPackets_v15_2 : public IUnknown
{
public:
virtual HRESULT GetPacketIterator (/* out */ IDeckLinkAncillaryPacketIterator_v15_2** iterator) = 0;
virtual HRESULT GetFirstPacketByID (/* in */ uint8_t DID, /* in */ uint8_t SDID, /* out */ IDeckLinkAncillaryPacket_v15_2** packet) = 0;
virtual HRESULT AttachPacket (/* in */ IDeckLinkAncillaryPacket_v15_2* packet) = 0; // Implement IDeckLinkAncillaryPacket to output your own
virtual HRESULT DetachPacket (/* in */ IDeckLinkAncillaryPacket_v15_2* packet) = 0;
virtual HRESULT DetachAllPackets (void) = 0;
protected:
virtual ~IDeckLinkVideoFrameAncillaryPackets_v15_2 () {} // call Release method to drop reference count
};
#endif /* defined(__cplusplus) */
#endif /* defined(BMD_DECKLINKAPI_H) */

View file

@ -0,0 +1,189 @@
/* -LICENSE-START-
** Copyright (c) 2025 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation (the
** "Software") to use, reproduce, display, distribute, sub-license, execute,
** and transmit the Software, and to prepare derivative works of the Software,
** and to permit third-parties to whom the Software is furnished to do so, in
** accordance with:
**
** (1) if the Software is obtained from Blackmagic Design, the End User License
** Agreement for the Software Development Kit (EULA) available at
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
**
** (2) if the Software is obtained from any third party, such licensing terms
** as notified by that third party,
**
** and all subject to the following:
**
** (3) the copyright notices in the Software and this entire statement,
** including the above license grant, this restriction and the following
** disclaimer, must be included in all copies of the Software, in whole or in
** part, and all derivative works of the Software, unless such copies or
** derivative works are solely in the form of machine-executable object code
** generated by a source language processor.
**
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
**
** A copy of the Software is available free of charge at
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
**
** -LICENSE-END-
*/
#pragma once
#include "DeckLinkAPI.h"
// Interface ID Declarations
BMD_CONST REFIID IID_IDeckLinkStatus_v15_3_1 = /* 5F558200-4028-49BC-BEAC-DB3FA4A96E46 */ { 0x5F,0x55,0x82,0x00,0x40,0x28,0x49,0xBC,0xBE,0xAC,0xDB,0x3F,0xA4,0xA9,0x6E,0x46 };
BMD_CONST REFIID IID_IDeckLinkVideoBuffer_v15_3_1 = /* CCB4B64A-5C86-4E02-B778-885D352709FE */ { 0xCC,0xB4,0xB6,0x4A,0x5C,0x86,0x4E,0x02,0xB7,0x78,0x88,0x5D,0x35,0x27,0x09,0xFE };
BMD_CONST REFIID IID_IDeckLinkVideoBufferAllocator_v15_3_1 = /* 3481A4DF-2B11-4E55-AC61-836B87985E9A */ { 0x34,0x81,0xA4,0xDF,0x2B,0x11,0x4E,0x55,0xAC,0x61,0x83,0x6B,0x87,0x98,0x5E,0x9A };
BMD_CONST REFIID IID_IDeckLinkVideoBufferAllocatorProvider_v15_3_1 = /* 08B80403-BFF2-49D0-B448-8C908B9E9FC9 */ { 0x08,0xB8,0x04,0x03,0xBF,0xF2,0x49,0xD0,0xB4,0x48,0x8C,0x90,0x8B,0x9E,0x9F,0xC9 };
BMD_CONST REFIID IID_IDeckLinkVideoConversion_v15_3_1 = /* A48755D9-8BD5-4727-A1E9-069FDEDBA6E9 */ { 0xA4,0x87,0x55,0xD9,0x8B,0xD5,0x47,0x27,0xA1,0xE9,0x06,0x9F,0xDE,0xDB,0xA6,0xE9 };
BMD_CONST REFIID IID_IDeckLinkProfileAttributes_v15_3_1 = /* 17D4BF8E-4911-473A-80A0-731CF6FF345B */ { 0x17,0xD4,0xBF,0x8E,0x49,0x11,0x47,0x3A,0x80,0xA0,0x73,0x1C,0xF6,0xFF,0x34,0x5B };
BMD_CONST REFIID IID_IDeckLinkNotification_v15_3_1 = /* B85DF4C8-BDF5-47C1-8064-28162EBDD4EB */ { 0xB8,0x5D,0xF4,0xC8,0xBD,0xF5,0x47,0xC1,0x80,0x64,0x28,0x16,0x2E,0xBD,0xD4,0xEB };
#if defined(__cplusplus)
/* Enum BMDDeckLinkStatusID_v15_3_1 - DeckLink Status ID */
typedef uint32_t BMDDeckLinkStatusID_v15_3_1;
enum _BMDDeckLinkStatusID_v15_3_1
{
/* Integers */
bmdDeckLinkStatusDeviceTemperature_v15_3_1 = /* 'dtmp' */ 0x64746D70,
bmdDeckLinkStatusEthernetLink_v15_3_1 = /* 'sels' */ 0x73656C73,
bmdDeckLinkStatusEthernetLinkMbps_v15_3_1 = /* 'sesp' */ 0x73657370,
/* Strings */
bmdDeckLinkStatusEthernetLocalIPAddress_v15_3_1 = /* 'seip' */ 0x73656970,
bmdDeckLinkStatusEthernetSubnetMask_v15_3_1 = /* 'sesm' */ 0x7365736D,
bmdDeckLinkStatusEthernetGatewayIPAddress_v15_3_1 = /* 'segw' */ 0x73656777,
bmdDeckLinkStatusEthernetPrimaryDNS_v15_3_1 = /* 'sepd' */ 0x73657064,
bmdDeckLinkStatusEthernetSecondaryDNS_v15_3_1 = /* 'sesd' */ 0x73657364,
bmdDeckLinkStatusEthernetVideoOutputAddress_v15_3_1 = /* 'soav' */ 0x736F6176,
bmdDeckLinkStatusEthernetAudioOutputAddress_v15_3_1 = /* 'soaa' */ 0x736F6161,
bmdDeckLinkStatusEthernetAncillaryOutputAddress_v15_3_1 = /* 'soaA' */ 0x736F6141,
};
/* Enum BMDDeckLinkAttributeID - DeckLink Attribute ID */
typedef uint32_t BMDDeckLinkAttributeID_v15_3_1;
enum _BMDDeckLinkAttributeID_v15_3_1
{
/* Strings */
BMDDeckLinkEthernetMACAddress_v15_3_1 = /* 'eMAC' */ 0x654D4143,
};
/* Interface IDeckLinkStatus_v15_3_1 - DeckLink Status interface */
class BMD_PUBLIC IDeckLinkStatus_v15_3_1 : public IUnknown
{
public:
virtual HRESULT GetFlag (/* in */ BMDDeckLinkStatusID statusID, /* out */ bool* value) = 0;
virtual HRESULT GetInt (/* in */ BMDDeckLinkStatusID statusID, /* out */ int64_t* value) = 0;
virtual HRESULT GetFloat (/* in */ BMDDeckLinkStatusID statusID, /* out */ double* value) = 0;
virtual HRESULT GetString (/* in */ BMDDeckLinkStatusID statusID, /* out */ const char** value) = 0;
virtual HRESULT GetBytes (/* in */ BMDDeckLinkStatusID statusID, /* out */ void* buffer, /* in, out */ uint32_t* bufferSize) = 0;
protected:
virtual ~IDeckLinkStatus_v15_3_1 () {} // call Release method to drop reference count
};
/* Interface IDeckLinkVideoBuffer_v15_3_1 - Interface to encapsulate a video frame buffer; can be caller-implemented. */
class BMD_PUBLIC IDeckLinkVideoBuffer_v15_3_1 : public IUnknown
{
public:
virtual HRESULT GetBytes (/* out */ void** buffer) = 0;
virtual HRESULT StartAccess (/* in */ BMDBufferAccessFlags flags) = 0;
virtual HRESULT EndAccess (/* in */ BMDBufferAccessFlags flags) = 0;
protected:
virtual ~IDeckLinkVideoBuffer_v15_3_1 () {} // call Release method to drop reference count
};
/* Interface IDeckLinkVideoBufferAllocator_v15_3_1 - Buffer allocator for video. */
class BMD_PUBLIC IDeckLinkVideoBufferAllocator_v15_3_1 : public IUnknown
{
public:
virtual HRESULT AllocateVideoBuffer (/* out */ IDeckLinkVideoBuffer_v15_3_1** allocatedBuffer) = 0;
protected:
virtual ~IDeckLinkVideoBufferAllocator_v15_3_1 () {} // call Release method to drop reference count
};
/* Interface IDeckLinkVideoBufferAllocatorProvider_v15_3_1 - Allows EnableVideoInputWithAllocatorProvider to obtain allocators */
class BMD_PUBLIC IDeckLinkVideoBufferAllocatorProvider_v15_3_1 : public IUnknown
{
public:
virtual HRESULT GetVideoBufferAllocator (/* in */ uint32_t bufferSize, /* in */ uint32_t width, /* in */ uint32_t height, /* in */ uint32_t rowBytes, /* in */ BMDPixelFormat pixelFormat, /* out */ IDeckLinkVideoBufferAllocator_v15_3_1** allocator) = 0;
protected:
virtual ~IDeckLinkVideoBufferAllocatorProvider_v15_3_1 () {} // call Release method to drop reference count
};
/* Interface IDeckLinkVideoConversion_v15_3_1 */
class BMD_PUBLIC IDeckLinkVideoConversion_v15_3_1 : public IUnknown
{
public:
virtual HRESULT ConvertFrame (/* in */ IDeckLinkVideoFrame* srcFrame, /* in */ IDeckLinkVideoFrame* dstFrame) = 0;
virtual HRESULT ConvertNewFrame (/* in */ IDeckLinkVideoFrame* srcFrame, /* in */ BMDPixelFormat dstPixelFormat, /* in */ BMDColorspace dstColorspace, /* in */ IDeckLinkVideoBuffer_v15_3_1* dstBuffer, /* out */ IDeckLinkVideoFrame** dstFrame) = 0;
protected:
virtual ~IDeckLinkVideoConversion_v15_3_1 () {} // call Release method to drop reference count
};
/* Interface IDeckLinkProfileAttributes_v15_3_1 - Created by QueryInterface from an IDeckLinkProfile, or from IDeckLink. When queried from IDeckLink, interrogates the active profile */
class BMD_PUBLIC IDeckLinkProfileAttributes_v15_3_1 : public IUnknown
{
public:
virtual HRESULT GetFlag (/* in */ BMDDeckLinkAttributeID cfgID, /* out */ bool* value) = 0;
virtual HRESULT GetInt (/* in */ BMDDeckLinkAttributeID cfgID, /* out */ int64_t* value) = 0;
virtual HRESULT GetFloat (/* in */ BMDDeckLinkAttributeID cfgID, /* out */ double* value) = 0;
virtual HRESULT GetString (/* in */ BMDDeckLinkAttributeID cfgID, /* out */ const char** value) = 0;
protected:
virtual ~IDeckLinkProfileAttributes_v15_3_1 () {} // call Release method to drop reference count
};
/* Interface IDeckLinkNotification_v15_3_1 - DeckLink Notification interface */
class BMD_PUBLIC IDeckLinkNotification_v15_3_1 : public IUnknown
{
public:
virtual HRESULT Subscribe (/* in */ BMDNotifications topic, /* in */ IDeckLinkNotificationCallback *theCallback) = 0;
virtual HRESULT Unsubscribe (/* in */ BMDNotifications topic, /* in */ IDeckLinkNotificationCallback *theCallback) = 0;
protected:
virtual ~IDeckLinkNotification_v15_3_1 () {} // call Release method to drop reference count
};
/* Functions */
extern "C"
{
BMD_PUBLIC IDeckLinkIterator* CreateDeckLinkIteratorInstance_v15_3_1(void);
BMD_PUBLIC IDeckLinkDiscovery* CreateDeckLinkDiscoveryInstance_v15_3_1(void);
BMD_PUBLIC IDeckLinkAPIInformation* CreateDeckLinkAPIInformationInstance_v15_3_1(void);
BMD_PUBLIC IDeckLinkGLScreenPreviewHelper* CreateOpenGLScreenPreviewHelper_v15_3_1(void);
BMD_PUBLIC IDeckLinkGLScreenPreviewHelper* CreateOpenGL3ScreenPreviewHelper_v15_3_1(void); // Requires OpenGL 3.2 support and provides improved performance and color handling
BMD_PUBLIC IDeckLinkVideoConversion_v15_3_1* CreateVideoConversionInstance_v15_3_1(void);
BMD_PUBLIC IDeckLinkVideoFrameAncillaryPackets* CreateVideoFrameAncillaryPacketsInstance_v15_3_1(void); // For use when creating a custom IDeckLinkVideoFrame without wrapping IDeckLinkOutput::CreateVideoFrame
}
#endif // defined(__cplusplus)

View file

@ -0,0 +1,116 @@
/* -LICENSE-START-
** Copyright (c) 2009 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation (the
** "Software") to use, reproduce, display, distribute, sub-license, execute,
** and transmit the Software, and to prepare derivative works of the Software,
** and to permit third-parties to whom the Software is furnished to do so, in
** accordance with:
**
** (1) if the Software is obtained from Blackmagic Design, the End User License
** Agreement for the Software Development Kit ("EULA") available at
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
**
** (2) if the Software is obtained from any third party, such licensing terms
** as notified by that third party,
**
** and all subject to the following:
**
** (3) the copyright notices in the Software and this entire statement,
** including the above license grant, this restriction and the following
** disclaimer, must be included in all copies of the Software, in whole or in
** part, and all derivative works of the Software, unless such copies or
** derivative works are solely in the form of machine-executable object code
** generated by a source language processor.
**
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
**
** A copy of the Software is available free of charge at
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
**
** -LICENSE-END-
*/
#ifndef __LINUX_COM_H_
#define __LINUX_COM_H_
struct REFIID
{
unsigned char byte0;
unsigned char byte1;
unsigned char byte2;
unsigned char byte3;
unsigned char byte4;
unsigned char byte5;
unsigned char byte6;
unsigned char byte7;
unsigned char byte8;
unsigned char byte9;
unsigned char byte10;
unsigned char byte11;
unsigned char byte12;
unsigned char byte13;
unsigned char byte14;
unsigned char byte15;
};
typedef REFIID CFUUIDBytes;
#define CFUUIDGetUUIDBytes(x) x
typedef int HRESULT;
typedef unsigned long ULONG;
typedef void *LPVOID;
#define SUCCEEDED(Status) ((HRESULT)(Status) >= 0)
#define FAILED(Status) ((HRESULT)(Status)<0)
#define IS_ERROR(Status) ((unsigned long)(Status) >> 31 == SEVERITY_ERROR)
#define HRESULT_CODE(hr) ((hr) & 0xFFFF)
#define HRESULT_FACILITY(hr) (((hr) >> 16) & 0x1fff)
#define HRESULT_SEVERITY(hr) (((hr) >> 31) & 0x1)
#define SEVERITY_SUCCESS 0
#define SEVERITY_ERROR 1
#define MAKE_HRESULT(sev,fac,code) ((HRESULT) (((unsigned long)(sev)<<31) | ((unsigned long)(fac)<<16) | ((unsigned long)(code))) )
#define S_OK ((HRESULT)0x00000000L)
#define S_FALSE ((HRESULT)0x00000001L)
#define E_UNEXPECTED ((HRESULT)0x8000FFFFL)
#define E_NOTIMPL ((HRESULT)0x80000001L)
#define E_OUTOFMEMORY ((HRESULT)0x80000002L)
#define E_INVALIDARG ((HRESULT)0x80000003L)
#define E_NOINTERFACE ((HRESULT)0x80000004L)
#define E_POINTER ((HRESULT)0x80000005L)
#define E_HANDLE ((HRESULT)0x80000006L)
#define E_ABORT ((HRESULT)0x80000007L)
#define E_FAIL ((HRESULT)0x80000008L)
#define E_ACCESSDENIED ((HRESULT)0x80000009L)
#define STDMETHODCALLTYPE
#define IID_IUnknown (REFIID){0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xC0,0x00,0x00,0x00,0x00,0x00,0x00,0x46}
#define IUnknownUUID IID_IUnknown
#ifndef BMD_PUBLIC
#define BMD_PUBLIC
#endif
#ifdef __cplusplus
class BMD_PUBLIC IUnknown
{
public:
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, LPVOID *ppv) = 0;
virtual ULONG STDMETHODCALLTYPE AddRef(void) = 0;
virtual ULONG STDMETHODCALLTYPE Release(void) = 0;
};
#endif
#endif

File diff suppressed because it is too large Load diff

View file

@ -63,6 +63,13 @@ async function bootstrapAutoStart() {
const streamKey = envOpt('STREAM_KEY'); const streamKey = envOpt('STREAM_KEY');
const sourceUrl = envOpt('SOURCE_URL'); const sourceUrl = envOpt('SOURCE_URL');
const device = envInt('DEVICE_INDEX'); const device = envInt('DEVICE_INDEX');
// SOURCE_CONFIG is the recorder's source_config JSON (set by recorders.js).
// For deltacast it carries the capture channel (`port`) and optional `board`.
let sourceConfig = {};
try { sourceConfig = JSON.parse(process.env.SOURCE_CONFIG || '{}') || {}; }
catch (e) { console.warn('[bootstrap] bad SOURCE_CONFIG JSON:', e.message); }
const port = Number.isInteger(sourceConfig.port) ? sourceConfig.port : undefined;
const board = Number.isInteger(sourceConfig.board) ? sourceConfig.board : undefined;
console.log(`[bootstrap] starting ${sourceType} ingest (listen=${listen} port=${listenPort || 'n/a'})...`); console.log(`[bootstrap] starting ${sourceType} ingest (listen=${listen} port=${listenPort || 'n/a'})...`);
try { try {
@ -72,6 +79,8 @@ async function bootstrapAutoStart() {
binId: envOpt('BIN_ID') || null, binId: envOpt('BIN_ID') || null,
clipName, clipName,
device, device,
port,
board,
sourceType, sourceType,
sourceUrl, sourceUrl,
listen, listen,
@ -135,6 +144,15 @@ async function gracefulShutdown(signal) {
console.error('[shutdown] failed to flag empty asset:', e.message); console.error('[shutdown] failed to flag empty asset:', e.message);
} }
} }
} else if (completed.growingPath) {
// Growing-files recorder: the master lives on the SMB share as a .ts,
// NOT in S3 yet. The promotion worker (which watches the same share)
// uploads it to S3 and enqueues the proxy from the real, finalized key.
// We must NOT call /finalize here: that sets original_s3_key to a key
// that doesn't exist yet and enqueues a proxy that instantly fails with
// "unable to open the file on disk." Leave the asset 'live' for the
// promotion worker to flip to 'ready'.
console.log(`[shutdown] growing capture finalized on share (${completed.growingPath}); leaving promotion worker to upload + proxy`);
} else if (liveAssetId) { } else if (liveAssetId) {
// Finalise the pre-created live asset by id (avoids POST / 409 collision). // Finalise the pre-created live asset by id (avoids POST / 409 collision).
try { try {

View file

@ -325,9 +325,13 @@ router.post('/start', async (req, res) => {
error: `${source_type.toUpperCase()} caller mode requires: source_url`, error: `${source_type.toUpperCase()} caller mode requires: source_url`,
}); });
} }
} else if (source_type === 'deltacast') {
if (device === undefined || device === null) {
return res.status(400).json({ error: 'deltacast source requires: device (board/port index)' });
}
} else { } else {
return res.status(400).json({ return res.status(400).json({
error: `Unknown source_type: ${source_type}. Must be sdi, srt, or rtmp`, error: `Unknown source_type: ${source_type}. Must be sdi, srt, rtmp, or deltacast`,
}); });
} }

View file

@ -0,0 +1,10 @@
-- Migration 031 — Add last_seen_at to cluster_nodes
--
-- Playout failover (routes/playout.js restartChannel) queries cluster_nodes.last_seen_at
-- to find healthy nodes for channel re-placement. Column was missing from original
-- cluster schema; heartbeat endpoint updates it via /cluster/heartbeat.
ALTER TABLE cluster_nodes ADD COLUMN IF NOT EXISTS last_seen_at TIMESTAMPTZ;
-- Backfill existing nodes to NOW() so they're immediately eligible for failover
UPDATE cluster_nodes SET last_seen_at = NOW() WHERE last_seen_at IS NULL;

View file

@ -0,0 +1,10 @@
-- Migration 032: Per-recorder GPU affinity (Issue #167)
-- Adds a nullable GPU UUID to the recorders table so each recorder can be
-- pinned to a specific GPU on its node. The value is passed through to the
-- node-agent sidecar-start payload and becomes NVIDIA_VISIBLE_DEVICES for the
-- capture container. NULL = legacy behavior (NVIDIA_VISIBLE_DEVICES=all, i.e.
-- every GPU visible). Accepts an nvidia-smi GPU UUID (e.g. "GPU-xxxx") or a
-- numeric index string.
ALTER TABLE recorders
ADD COLUMN IF NOT EXISTS gpu_uuid TEXT DEFAULT NULL;

View file

@ -0,0 +1,52 @@
-- Migration 033 — SCTE-35 ad-break markers for playout.
--
-- Adds the missing SCTE-35 splice feature to the playout (MCR) subsystem. An
-- operator can either schedule an ad break on a channel's timeline (relative to
-- the active playlist position, or at a wall-clock time) or fire one immediately
-- ("splice now"). Each break is recorded here and, when fired, also written to
-- the append-only as-run log so it shows in the compliance record alongside the
-- clips that aired.
--
-- type:
-- splice_insert — a scheduled break (out → return), duration_s seconds long
-- immediate — fire-now splice (operator pressed "Trigger ad break now")
-- splice_out — open-ended avail out (provider break start)
-- splice_in — return-to-network (provider break end)
--
-- status: pending → fired (when the engine acts on it) → done (when the break
-- window has elapsed). cancelled is set if the operator removes a pending break.
--
-- The engine (services/playout) acts on a break by logging the cue, marking the
-- as-run row, and — where the output path supports it — injecting a real
-- SCTE-35 cue (see playout-manager.triggerScte for the injection point/TODO).
CREATE TABLE IF NOT EXISTS playout_scte_breaks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
channel_id UUID NOT NULL REFERENCES playout_channels(id) ON DELETE CASCADE,
-- Position on the active playlist this break should fire after (0-based item
-- index). NULL for immediate/wall-clock breaks.
playlist_pos INTEGER,
-- Wall-clock fire time for scheduled breaks. NULL for immediate breaks.
scheduled_at TIMESTAMPTZ,
duration_s INTEGER NOT NULL DEFAULT 30,
-- SCTE-35 event id (the splice_event_id carried in the cue). Auto-assigned.
event_id INTEGER NOT NULL DEFAULT 1,
type TEXT NOT NULL DEFAULT 'splice_insert',
status TEXT NOT NULL DEFAULT 'pending',
fired_at TIMESTAMPTZ,
created_by UUID REFERENCES users(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CHECK (type IN ('splice_insert','immediate','splice_out','splice_in')),
CHECK (status IN ('pending','fired','done','cancelled'))
);
CREATE INDEX IF NOT EXISTS idx_playout_scte_channel ON playout_scte_breaks (channel_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_playout_scte_status ON playout_scte_breaks (status);
-- As-run gains a 'scte' result so fired breaks land in the compliance log next to
-- the clips. The original migration constrained result to played/skipped/error;
-- widen it.
ALTER TABLE playout_as_run DROP CONSTRAINT IF EXISTS playout_as_run_result_check;
ALTER TABLE playout_as_run ADD CONSTRAINT playout_as_run_result_check
CHECK (result IN ('played','skipped','error','scte'));

View file

@ -1,4 +1,16 @@
import { Pool } from 'pg'; import { Pool, types } from 'pg';
// node-postgres returns BIGINT (int8, OID 20) as a *string* by default, because
// a 64-bit integer can exceed JS Number.MAX_SAFE_INTEGER. Our int8 columns
// (duration_ms, file_size, …) are always well within 2^53, so a string here is
// pure footgun: it breaks any consumer that does arithmetic or comparison on the
// value (e.g. `duration_ms + x` silently string-concatenates, sorts go
// lexicographic, `!ms`/`Math.round` edge cases). Parse int8 to a real Number so
// the API always emits numeric duration_ms/file_size in its JSON.
//
// 20 = int8/bigint OID. Values above Number.MAX_SAFE_INTEGER would lose
// precision, but no column in this schema ever reaches that range.
types.setTypeParser(20, (val) => (val === null ? null : parseInt(val, 10)));
// Prefer DATABASE_URL (set in docker-compose) over individual DB_* vars // Prefer DATABASE_URL (set in docker-compose) over individual DB_* vars
const pool = process.env.DATABASE_URL const pool = process.env.DATABASE_URL

View file

@ -41,18 +41,12 @@ import { startCleanupLoop } from './tasks/cleanupTempSegments.js';
const app = express(); const app = express();
const PORT = process.env.PORT || 3000; const PORT = process.env.PORT || 3000;
// ── Middleware ────────────────────────────────────────────────────────────────
// Tightened CORS — once cookies carry authority, `origin: true` would let
// any site forge requests with the cookie. Drive the allowlist from env.
const allowedOrigins = (process.env.ALLOWED_ORIGINS || '') const allowedOrigins = (process.env.ALLOWED_ORIGINS || '')
.split(',').map(s => s.trim()).filter(Boolean); .split(',').map(s => s.trim()).filter(Boolean);
app.use(cors({ app.use(cors({
origin: (origin, cb) => { origin: (origin, cb) => {
// No Origin header (same-origin or curl) — allow.
if (!origin) return cb(null, true); if (!origin) return cb(null, true);
if (allowedOrigins.length === 0 || allowedOrigins.includes(origin)) return cb(null, true); if (allowedOrigins.length === 0 || allowedOrigins.includes(origin)) return cb(null, true);
// Reject cleanly: omit the Allow-Origin header so the browser surfaces
// a real CORS error instead of a 500 from a thrown Error in the callback.
console.warn('[cors] rejected origin:', origin); console.warn('[cors] rejected origin:', origin);
return cb(null, false); return cb(null, false);
}, },
@ -60,14 +54,8 @@ app.use(cors({
})); }));
app.use(express.json({ limit: '50mb' })); app.use(express.json({ limit: '50mb' }));
// Trust the reverse proxy only when explicitly told to (production HTTPS).
if (process.env.TRUST_PROXY === 'true') app.set('trust proxy', 1); if (process.env.TRUST_PROXY === 'true') app.set('trust proxy', 1);
// HSTS — once a browser has seen this header over HTTPS for dragonflight.live,
// it auto-upgrades every future http:// request to https:// before hitting the
// wire. Cookies are Secure-only (below) and the CORS allowlist rejects HTTP,
// so without HSTS a user who lands on http:// silently can't log in.
// Only emit on actual HTTPS responses; req.secure honors trust proxy + X-Forwarded-Proto.
if (process.env.AUTH_ENABLED === 'true') { if (process.env.AUTH_ENABLED === 'true') {
app.use((req, res, next) => { app.use((req, res, next) => {
if (req.secure) res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'); if (req.secure) res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
@ -75,17 +63,13 @@ if (process.env.AUTH_ENABLED === 'true') {
}); });
} }
// Hard-fail when production-mode auth has no stable session secret. Without
// this, express-session falls back to an in-memory random secret which
// invalidates every session on restart and breaks multi-node deployments.
if (process.env.AUTH_ENABLED === 'true' && !process.env.SESSION_SECRET) { if (process.env.AUTH_ENABLED === 'true' && !process.env.SESSION_SECRET) {
console.error('[fatal] SESSION_SECRET is required when AUTH_ENABLED=true'); console.error('[fatal] SESSION_SECRET is required when AUTH_ENABLED=true');
process.exit(1); process.exit(1);
} }
// Session — actually wired this time. See specs/2026-05-27-auth-system-design.md.
app.use(session({ app.use(session({
store: new PgStore({ pool, tableName: 'sessions', pruneSessionInterval: 60 * 15 /* seconds = 15 min */ }), store: new PgStore({ pool, tableName: 'sessions', pruneSessionInterval: 60 * 15 }),
secret: process.env.SESSION_SECRET, secret: process.env.SESSION_SECRET,
name: 'dragonflight.sid', name: 'dragonflight.sid',
cookie: { cookie: {
@ -95,36 +79,26 @@ app.use(session({
path: '/', path: '/',
maxAge: 8 * 3600 * 1000, maxAge: 8 * 3600 * 1000,
}, },
rolling: false, // sliding renewal handled in requireAuth so idle + absolute can be enforced separately rolling: false,
resave: false, resave: false,
saveUninitialized: false, saveUninitialized: false,
})); }));
// ── Health ────────────────────────────────────────────────────────────────────
app.get('/health', (_req, res) => res.json({ status: 'ok' })); app.get('/health', (_req, res) => res.json({ status: 'ok' }));
// ── Auth gate ─────────────────────────────────────────────────────────────────
// req.path is relative to the /api/v1 mount, so /auth/login NOT /api/v1/auth/login.
const UNAUTH_PATHS = new Set([ const UNAUTH_PATHS = new Set([
'/auth/login', '/auth/login/totp', '/auth/setup', '/auth/setup-required', '/auth/login', '/auth/login/totp', '/auth/setup', '/auth/setup-required',
'/auth/google', '/auth/google/callback', '/auth/google/enabled', '/auth/google', '/auth/google/callback', '/auth/google/enabled',
]); ]);
// node-agent now authenticates /cluster/heartbeat with a bound api_token
// (migration 019 + bound_hostname on the token). requireAuth handles the
// bearer lookup and sets req.tokenBoundHostname; the heartbeat handler in
// routes/cluster.js verifies body.hostname matches that binding.
app.use('/api/v1', requireUiHeader); app.use('/api/v1', requireUiHeader);
app.use('/api/v1', (req, res, next) => { app.use('/api/v1', (req, res, next) => {
if (UNAUTH_PATHS.has(req.path)) return next(); if (UNAUTH_PATHS.has(req.path)) return next();
return requireAuth(req, res, next); return requireAuth(req, res, next);
}); });
// ── API Routes ────────────────────────────────────────────────────────────────
app.use('/api/v1/auth', authRouter); app.use('/api/v1/auth', authRouter);
// User and group administration is admin-only (RBAC v2). The auth gate above
// already established req.user; requireAdmin rejects non-admins with 403.
app.use('/api/v1/auth/users', requireAdmin, usersRouter); app.use('/api/v1/auth/users', requireAdmin, usersRouter);
app.use('/api/v1/users', requireAdmin, usersRouter); // alias for the SPA Users page app.use('/api/v1/users', requireAdmin, usersRouter);
app.use('/api/v1/auth/tokens', requireAuth, tokensRouter); app.use('/api/v1/auth/tokens', requireAuth, tokensRouter);
app.use('/api/v1/assets', assetsRouter); app.use('/api/v1/assets', assetsRouter);
app.use('/api/v1/projects', projectsRouter); app.use('/api/v1/projects', projectsRouter);
@ -147,21 +121,14 @@ app.use('/api/v1/assets/:assetId/comments', commentsRouter);
app.use('/api/v1/imports', importsRouter); app.use('/api/v1/imports', importsRouter);
app.use('/api/v1/storage', storageRouter); app.use('/api/v1/storage', storageRouter);
// ── Error handler ─────────────────────────────────────────────────────────────
app.use(errorHandler); app.use(errorHandler);
// ── Start ────────────────────────────────────────────────────────────────────
import { readdirSync, readFileSync } from 'node:fs'; import { readdirSync, readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path'; import { dirname, join } from 'node:path';
const __dirnameMig = dirname(fileURLToPath(import.meta.url)); const __dirnameMig = dirname(fileURLToPath(import.meta.url));
async function runMigrations() { async function runMigrations() {
// Issue #107 — previously the loop swallowed errors and let the server boot
// on a half-migrated schema. Now: track applied migrations in a table, run
// every pending one inside a transaction, and exit non-zero on failure so
// the orchestrator restarts (and so an operator notices) instead of serving
// 500s for the next month.
const dir = join(__dirnameMig, 'db', 'migrations'); const dir = join(__dirnameMig, 'db', 'migrations');
let files = []; let files = [];
try { files = readdirSync(dir).filter(f => f.endsWith('.sql')).sort(); } catch { return; } try { files = readdirSync(dir).filter(f => f.endsWith('.sql')).sort(); } catch { return; }
@ -174,7 +141,6 @@ async function runMigrations() {
) )
`); `);
// Allow forcing a re-run via env when iterating locally.
const force = process.env.MIGRATIONS_FORCE === '1'; const force = process.env.MIGRATIONS_FORCE === '1';
const allowFailures = process.env.MIGRATIONS_ALLOW_FAILURES === '1'; const allowFailures = process.env.MIGRATIONS_ALLOW_FAILURES === '1';
@ -200,7 +166,6 @@ async function runMigrations() {
console.error('[migration] FAILED ' + f + ': ' + err.message); console.error('[migration] FAILED ' + f + ': ' + err.message);
client.release(); client.release();
if (allowFailures) continue; if (allowFailures) continue;
// Hard fail — better to crash now than serve traffic on a broken schema.
console.error('[migration] aborting startup. Set MIGRATIONS_ALLOW_FAILURES=1 to override.'); console.error('[migration] aborting startup. Set MIGRATIONS_ALLOW_FAILURES=1 to override.');
process.exit(1); process.exit(1);
} }
@ -209,13 +174,9 @@ async function runMigrations() {
} }
await runMigrations(); await runMigrations();
// Load S3 config from DB so any settings saved via the Settings page override env vars
await loadS3ConfigFromDb(); await loadS3ConfigFromDb();
// ── Cluster self-heartbeat ────────────────────────────────────────────────────
function getLocalIp() { function getLocalIp() {
// Prefer an explicit override — useful when running inside Docker where
// os.networkInterfaces() returns container bridge IPs, not the host LAN IP.
if (process.env.NODE_IP) return process.env.NODE_IP; if (process.env.NODE_IP) return process.env.NODE_IP;
const ifaces = os.networkInterfaces(); const ifaces = os.networkInterfaces();
@ -227,9 +188,6 @@ function getLocalIp() {
return '127.0.0.1'; return '127.0.0.1';
} }
// Detect NVIDIA GPUs available to this container via nvidia-smi.
// Returns an array like [{ index: 0, name: 'Tesla P4', memory_mb: 7680 }, ...]
// or an empty array if nvidia-smi is unavailable or no GPUs found.
function detectGpus() { function detectGpus() {
return new Promise(resolve => { return new Promise(resolve => {
exec( exec(
@ -251,6 +209,10 @@ function detectGpus() {
}); });
} }
// Primary mam-api node self-registers in cluster_nodes every 30s. Must write
// BOTH last_seen (legacy column) and last_seen_at (added by mig 031, used by
// playout failover) — otherwise the primary appears stale to the failover
// query and channels get re-placed off it incorrectly.
async function selfHeartbeat() { async function selfHeartbeat() {
const load = os.loadavg()[0]; const load = os.loadavg()[0];
const total = os.totalmem(); const total = os.totalmem();
@ -262,14 +224,15 @@ async function selfHeartbeat() {
pool.query( pool.query(
`INSERT INTO cluster_nodes `INSERT INTO cluster_nodes
(hostname, ip_address, role, version, api_url, (hostname, ip_address, role, version, api_url,
cpu_usage, mem_used_mb, mem_total_mb, capabilities, last_seen) cpu_usage, mem_used_mb, mem_total_mb, capabilities, last_seen, last_seen_at)
VALUES ($1,$2,'primary',$3,$4,$5,$6,$7,$8,NOW()) VALUES ($1,$2,'primary',$3,$4,$5,$6,$7,$8,NOW(),NOW())
ON CONFLICT (hostname) DO UPDATE SET ON CONFLICT (hostname) DO UPDATE SET
ip_address = EXCLUDED.ip_address, ip_address = EXCLUDED.ip_address,
cpu_usage = EXCLUDED.cpu_usage, cpu_usage = EXCLUDED.cpu_usage,
mem_used_mb = EXCLUDED.mem_used_mb, mem_used_mb = EXCLUDED.mem_used_mb,
mem_total_mb = EXCLUDED.mem_total_mb, mem_total_mb = EXCLUDED.mem_total_mb,
capabilities = EXCLUDED.capabilities, capabilities = EXCLUDED.capabilities,
last_seen_at = NOW(),
last_seen = NOW()`, last_seen = NOW()`,
[ [
process.env.NODE_HOSTNAME || os.hostname(), process.env.NODE_HOSTNAME || os.hostname(),
@ -294,39 +257,26 @@ const server = app.listen(PORT, () => {
if (process.env.AUTH_ENABLED === 'true' && process.env.TRUST_PROXY !== 'true') { if (process.env.AUTH_ENABLED === 'true' && process.env.TRUST_PROXY !== 'true') {
console.warn('[auth] WARNING: AUTH_ENABLED=true but TRUST_PROXY=false — req.ip will be the proxy IP, login rate-limit will throttle all clients together. Set TRUST_PROXY=true when behind nginx/HTTPS.'); console.warn('[auth] WARNING: AUTH_ENABLED=true but TRUST_PROXY=false — req.ip will be the proxy IP, login rate-limit will throttle all clients together. Set TRUST_PROXY=true when behind nginx/HTTPS.');
} }
// Boot the recorder scheduler tick loop after the HTTP server is live so
// the loop's self-calls to /recorders/:id/start|stop reach a ready socket.
startSchedulerLoop(); startSchedulerLoop();
// Boot the temp-segment cleanup loop (runs hourly).
startCleanupLoop(); startCleanupLoop();
}); });
// Issue #100 — graceful shutdown. Without this, `docker stop` (SIGTERM) killed
// the process mid-scheduler-tick, leaving Redis connections and Docker
// sockets dangling and producing partial DB writes. Now: stop the scheduler,
// finish in-flight HTTP requests, close PG/Redis pools, and exit cleanly
// (or hard-exit after 25 s if something is stuck).
let _shuttingDown = false; let _shuttingDown = false;
async function gracefulShutdown(signal) { async function gracefulShutdown(signal) {
if (_shuttingDown) return; if (_shuttingDown) return;
_shuttingDown = true; _shuttingDown = true;
console.log(`[shutdown] received ${signal} — closing gracefully…`); console.log(`[shutdown] received ${signal} — closing gracefully…`);
// Stop accepting new requests + wind down the scheduler tick.
try { stopSchedulerLoop(); } catch (_) {} try { stopSchedulerLoop(); } catch (_) {}
// Force-exit watchdog so a hung connection can't keep us alive forever.
const killSwitch = setTimeout(() => { const killSwitch = setTimeout(() => {
console.error('[shutdown] forced exit after 25s timeout'); console.error('[shutdown] forced exit after 25s timeout');
process.exit(1); process.exit(1);
}, 25_000); }, 25_000);
killSwitch.unref(); killSwitch.unref();
// Stop the HTTP server (waits for in-flight requests to finish).
await new Promise(resolve => server.close(resolve)); await new Promise(resolve => server.close(resolve));
// Close DB pool + S3 client + any other resources. Best-effort.
try { await pool.end(); } catch (e) { console.warn('[shutdown] pool.end:', e.message); } try { await pool.end(); } catch (e) { console.warn('[shutdown] pool.end:', e.message); }
console.log('[shutdown] clean exit'); console.log('[shutdown] clean exit');

View file

@ -742,11 +742,13 @@ router.get('/:id/live-path', async (req, res, next) => {
const cfg = {}; const cfg = {};
for (const { key, value } of s.rows) cfg[key] = value; for (const { key, value } of s.rows) cfg[key] = value;
if (!cfg.growing_smb_url) return res.status(409).json({ error: 'No SMB URL configured — set the editor SMB URL in Settings → Storage' }); if (!cfg.growing_smb_url) return res.status(409).json({ error: 'No SMB URL configured — set the editor SMB URL in Settings → Storage' });
const rec = await pool.query( // The growing master is ALWAYS MXF OP1a (XDCAM HD422) on the share, regardless
`SELECT recording_container FROM recorders WHERE current_session_id = $1 ORDER BY updated_at DESC LIMIT 1`, // of the recorder's configured finalized container — that is the format
[asset.id] // Premiere supports for edit-while-record growing files (incremental index
); // segments written into body partitions, readable with no footer). The file
const ext = rec.rows[0]?.recording_container || 'mov'; // on the share is `<clip>.mxf`. Keep this in lock-step with GROWING_EXT in
// services/capture/src/capture-manager.js.
const ext = 'mxf';
const smbRoot = cfg.growing_smb_url.replace(/\/+$/, ''); const smbRoot = cfg.growing_smb_url.replace(/\/+$/, '');
const winPath = smbRoot.replace(/^smb:\/\//, '\\\\').replace(/\//g, '\\') + `\\${asset.project_id}\\${asset.display_name}.${ext}`; const winPath = smbRoot.replace(/^smb:\/\//, '\\\\').replace(/\//g, '\\') + `\\${asset.project_id}\\${asset.display_name}.${ext}`;
const posix = smbRoot.replace(/^smb:\/\//, '//') + `/${asset.project_id}/${asset.display_name}.${ext}`; const posix = smbRoot.replace(/^smb:\/\//, '//') + `/${asset.project_id}/${asset.display_name}.${ext}`;

View file

@ -1,10 +1,27 @@
import express from 'express'; import express from 'express';
import http from 'http'; import http from 'http';
import os from 'os';
import pool from '../db/pool.js'; import pool from '../db/pool.js';
import { requireAdmin } from '../middleware/auth.js'; import { requireAdmin } from '../middleware/auth.js';
const router = express.Router(); const router = express.Router();
// Hostname the primary mam-api self-registers as (mirrors selfHeartbeat()).
const SELF_HOSTNAME = process.env.NODE_HOSTNAME || os.hostname();
// Format a process uptime (seconds) the way the Cluster UI expects — a short
// human string like "3d 4h" / "12m". Workers don't report uptime today, so the
// primary is the only row that populates this.
function formatUptime(seconds) {
const s = Math.floor(seconds);
const d = Math.floor(s / 86400);
const h = Math.floor((s % 86400) / 3600);
const m = Math.floor((s % 3600) / 60);
if (d > 0) return `${d}d ${h}h`;
if (h > 0) return `${h}h ${m}m`;
return `${m}m`;
}
// GET /onboard-info admin-only. Supplies the Add Node wizard with the bits it // GET /onboard-info admin-only. Supplies the Add Node wizard with the bits it
// needs to build a `curl … | bash` onboarding command: the primary API URL the // needs to build a `curl … | bash` onboarding command: the primary API URL the
// remote node-agent should heartbeat to, the raw URL of onboard-node.sh, and // remote node-agent should heartbeat to, the raw URL of onboard-node.sh, and
@ -55,7 +72,6 @@ function dockerRequest(path, method = 'GET', body = null) {
}); });
} }
// GET / list all registered cluster nodes with online status
router.get('/', async (req, res, next) => { router.get('/', async (req, res, next) => {
try { try {
const r = await pool.query( const r = await pool.query(
@ -64,25 +80,45 @@ router.get('/', async (req, res, next) => {
FROM cluster_nodes FROM cluster_nodes
ORDER BY registered_at ASC` ORDER BY registered_at ASC`
); );
res.json(r.rows.map(row => ({ res.json(r.rows.map(row => {
...row, const out = { ...row, online: Number(row.stale_seconds) < 120 };
online: Number(row.stale_seconds) < 120, // The primary (this mam-api host) does not heartbeat via the node-agent,
}))); // so its version/uptime are never populated. Self-populate them here so
// the Cluster screen renders them like worker nodes instead of dashes.
if (row.role === 'primary' && row.hostname === SELF_HOSTNAME) {
out.version = process.env.npm_package_version || row.version || null;
out.uptime = formatUptime(process.uptime());
}
return out;
}));
} catch (err) { next(err); } } catch (err) { next(err); }
}); });
// GET /containers list all containers on the local Docker host
router.get('/containers', async (req, res, next) => { router.get('/containers', async (req, res, next) => {
try { try {
const containers = await dockerRequest('/containers/json?all=true'); const containers = await dockerRequest('/containers/json?all=true');
if (!Array.isArray(containers)) return res.json([]); if (!Array.isArray(containers)) return res.json([]);
const out = containers.map(c => { const out = await Promise.all(containers.map(async c => {
const rawName = (c.Names[0] || '').replace(/^\//, ''); const rawName = (c.Names[0] || '').replace(/^\//, '');
const name = rawName.replace(/^wild-dragon-/, '').replace(/-\d+$/, ''); const name = rawName.replace(/^wild-dragon-/, '').replace(/-\d+$/, '');
const ports = (c.Ports || []) const ports = (c.Ports || [])
.filter(p => p.PublicPort) .filter(p => p.PublicPort)
.map(p => `${p.PublicPort}${p.PrivatePort}`) .map(p => `${p.PublicPort}${p.PrivatePort}`)
.join(', '); .join(', ');
// Live memory usage requires a per-container stats call (the list endpoint
// doesn't include it). One extra Docker call each, but the list is small.
// memory_stats.usage includes page cache; subtract it to match `docker stats`.
let memBytes = null;
if (c.State === 'running') {
try {
const stats = await dockerRequest(`/containers/${c.Id}/stats?stream=false`);
const ms = stats && stats.memory_stats;
if (ms && typeof ms.usage === 'number') {
const cache = (ms.stats && ms.stats.cache) || 0;
memBytes = ms.usage - cache;
}
} catch (_) { memBytes = null; }
}
return { return {
id: c.Id.slice(0, 12), id: c.Id.slice(0, 12),
name, name,
@ -92,9 +128,9 @@ router.get('/containers', async (req, res, next) => {
healthy: (c.Status || '').includes('healthy'), healthy: (c.Status || '').includes('healthy'),
ports, ports,
cpu: 0, cpu: 0,
mem: 0, memBytes,
}; };
}); }));
res.json(out); res.json(out);
} catch (err) { } catch (err) {
if (err.code === 'ENOENT' || err.code === 'EACCES') return res.json([]); if (err.code === 'ENOENT' || err.code === 'EACCES') return res.json([]);
@ -102,7 +138,6 @@ router.get('/containers', async (req, res, next) => {
} }
}); });
// POST /containers/:nameOrId/restart
router.post('/containers/:nameOrId/restart', async (req, res, next) => { router.post('/containers/:nameOrId/restart', async (req, res, next) => {
try { try {
await dockerRequest(`/containers/${encodeURIComponent(req.params.nameOrId)}/restart`, 'POST'); await dockerRequest(`/containers/${encodeURIComponent(req.params.nameOrId)}/restart`, 'POST');
@ -110,7 +145,6 @@ router.post('/containers/:nameOrId/restart', async (req, res, next) => {
} catch (err) { next(err); } } catch (err) { next(err); }
}); });
// POST /heartbeat upsert this node's registration (includes hardware capabilities)
router.post('/heartbeat', async (req, res, next) => { router.post('/heartbeat', async (req, res, next) => {
try { try {
const { const {
@ -122,11 +156,6 @@ router.post('/heartbeat', async (req, res, next) => {
if (!hostname) return res.status(400).json({ error: 'hostname is required' }); if (!hostname) return res.status(400).json({ error: 'hostname is required' });
// Issue #106 — any authenticated user used to be able to POST a heartbeat
// for an arbitrary hostname and overwrite the primary node's `api_url`,
// effectively hijacking job dispatch. Now: if the caller's token is bound
// to a hostname (node-agent tokens are bound at issue time), the body
// hostname must match. Admin users with no binding are allowed for ops.
if (process.env.AUTH_ENABLED === 'true') { if (process.env.AUTH_ENABLED === 'true') {
const bound = req.tokenBoundHostname; const bound = req.tokenBoundHostname;
if (bound && bound !== hostname) { if (bound && bound !== hostname) {
@ -146,8 +175,8 @@ router.post('/heartbeat', async (req, res, next) => {
const r = await pool.query( const r = await pool.query(
`INSERT INTO cluster_nodes `INSERT INTO cluster_nodes
(hostname, ip_address, role, version, api_url, (hostname, ip_address, role, version, api_url,
cpu_usage, mem_used_mb, mem_total_mb, last_seen, capabilities, metadata, metrics) cpu_usage, mem_used_mb, mem_total_mb, last_seen, last_seen_at, capabilities, metadata, metrics)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,NOW(),$9,$10,$11) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,NOW(),NOW(),$9,$10,$11)
ON CONFLICT (hostname) DO UPDATE SET ON CONFLICT (hostname) DO UPDATE SET
ip_address = EXCLUDED.ip_address, ip_address = EXCLUDED.ip_address,
role = EXCLUDED.role, role = EXCLUDED.role,
@ -157,6 +186,7 @@ router.post('/heartbeat', async (req, res, next) => {
mem_used_mb = EXCLUDED.mem_used_mb, mem_used_mb = EXCLUDED.mem_used_mb,
mem_total_mb = EXCLUDED.mem_total_mb, mem_total_mb = EXCLUDED.mem_total_mb,
last_seen = NOW(), last_seen = NOW(),
last_seen_at = NOW(),
capabilities = EXCLUDED.capabilities, capabilities = EXCLUDED.capabilities,
metadata = EXCLUDED.metadata, metadata = EXCLUDED.metadata,
metrics = COALESCE(EXCLUDED.metrics, cluster_nodes.metrics) metrics = COALESCE(EXCLUDED.metrics, cluster_nodes.metrics)
@ -179,42 +209,25 @@ router.post('/heartbeat', async (req, res, next) => {
} catch (err) { next(err); } } catch (err) { next(err); }
}); });
// GET /devices/blackmagic/signal live video-presence state for every
// DeckLink port across the cluster. For each port we check whether there is
// an active SDI recorder assigned to it and, if so, query the capture
// container for its real signal state (receiving / lost / connecting /
// error). Ports without a recorder get signal = 'no-recorder'.
//
// Response shape (array):
// { node_id, hostname, index, device, model,
// signal, framesReceived, currentFps, recorder_id, recorder_status }
router.get('/devices/blackmagic/signal', async (req, res, next) => { router.get('/devices/blackmagic/signal', async (req, res, next) => {
try { try {
// 1. Fetch all cluster nodes with DeckLink capabilities.
const nodesResult = await pool.query( const nodesResult = await pool.query(
`SELECT id, hostname, ip_address, api_url, capabilities, `SELECT id, hostname, ip_address, api_url, capabilities,
EXTRACT(EPOCH FROM (NOW() - last_seen)) AS stale_seconds EXTRACT(EPOCH FROM (NOW() - last_seen)) AS stale_seconds
FROM cluster_nodes FROM cluster_nodes
WHERE capabilities IS NOT NULL` WHERE capabilities IS NOT NULL`
); );
// 2. Fetch all SDI recorders that are pinned to a node+device_index.
const recResult = await pool.query( const recResult = await pool.query(
`SELECT id, name, status, container_id, node_id, device_index, `SELECT id, name, status, container_id, node_id, device_index,
source_config source_config
FROM recorders FROM recorders
WHERE source_type = 'sdi' AND node_id IS NOT NULL` WHERE source_type = 'sdi' AND node_id IS NOT NULL`
); );
// Build a fast lookup: "${node_id}:${device_index}" → recorder row.
const recByPort = new Map(); const recByPort = new Map();
for (const r of recResult.rows) { for (const r of recResult.rows) {
const devIdx = r.device_index ?? r.source_config?.device ?? 0; const devIdx = r.device_index ?? r.source_config?.device ?? 0;
recByPort.set(`${r.node_id}:${devIdx}`, r); recByPort.set(`${r.node_id}:${devIdx}`, r);
} }
// 3. For each port, determine signal state. We fire all capture-container
// fetches concurrently so the endpoint stays fast even with many ports.
const tasks = []; const tasks = [];
for (const node of nodesResult.rows) { for (const node of nodesResult.rows) {
const nodeOnline = Number(node.stale_seconds) < 120; const nodeOnline = Number(node.stale_seconds) < 120;
@ -222,79 +235,51 @@ router.get('/devices/blackmagic/signal', async (req, res, next) => {
const model = (node.capabilities && node.capabilities.blackmagic_model) || null; const model = (node.capabilities && node.capabilities.blackmagic_model) || null;
const localHostname = process.env.NODE_HOSTNAME || ''; const localHostname = process.env.NODE_HOSTNAME || '';
const isRemote = node.api_url && node.hostname !== localHostname; const isRemote = node.api_url && node.hostname !== localHostname;
bm.forEach((d, idx) => { bm.forEach((d, idx) => {
const portIndex = d.index !== undefined ? d.index : idx; const portIndex = d.index !== undefined ? d.index : idx;
const rec = recByPort.get(`${node.id}:${portIndex}`); const rec = recByPort.get(`${node.id}:${portIndex}`);
tasks.push((async () => { tasks.push((async () => {
const base = { const base = {
node_id: node.id, node_id: node.id, hostname: node.hostname, index: portIndex,
hostname: node.hostname, device: d.device || null, model, node_online: nodeOnline,
index: portIndex, recorder_id: rec ? rec.id : null, recorder_name: rec ? rec.name : null,
device: d.device || null,
model,
node_online: nodeOnline,
recorder_id: rec ? rec.id : null,
recorder_name: rec ? rec.name : null,
recorder_status: rec ? rec.status : null, recorder_status: rec ? rec.status : null,
signal: 'no-recorder', signal: 'no-recorder', framesReceived: null, currentFps: null,
framesReceived: null,
currentFps: null,
}; };
if (!rec || rec.status !== 'recording' || !rec.container_id) { if (!rec || rec.status !== 'recording' || !rec.container_id) {
// No active capture — if there's a recorder but it's not recording,
// report that; otherwise the port is unassigned.
if (rec && rec.status !== 'recording') base.signal = 'idle'; if (rec && rec.status !== 'recording') base.signal = 'idle';
return base; return base;
} }
// Active recording — query the capture container for real signal.
try { try {
let live = null; let live = null;
if (isRemote) { if (isRemote) {
const r = await fetch( const r = await fetch(`${node.api_url}/sidecar/${rec.container_id}/status`, { signal: AbortSignal.timeout(2500) });
`${node.api_url}/sidecar/${rec.container_id}/status`,
{ signal: AbortSignal.timeout(2500) }
);
if (r.ok) live = (await r.json()).live; if (r.ok) live = (await r.json()).live;
} else { } else {
const r = await fetch( const r = await fetch(`http://recorder-${rec.id}:3001/capture/status`, { signal: AbortSignal.timeout(2000) });
`http://recorder-${rec.id}:3001/capture/status`,
{ signal: AbortSignal.timeout(2000) }
);
if (r.ok) live = await r.json(); if (r.ok) live = await r.json();
} }
if (live && live.signal) { if (live && live.signal) {
base.signal = live.signal; base.signal = live.signal;
base.framesReceived = live.framesReceived ?? null; base.framesReceived = live.framesReceived ?? null;
base.currentFps = live.currentFps ?? null; base.currentFps = live.currentFps ?? null;
} else { } else { base.signal = 'connecting'; }
base.signal = 'connecting'; } catch (_) { base.signal = 'connecting'; }
}
} catch (_) {
base.signal = 'connecting';
}
return base; return base;
})()); })());
}); });
} }
const results = await Promise.all(tasks); const results = await Promise.all(tasks);
res.json(results); res.json(results);
} catch (err) { next(err); } } catch (err) { next(err); }
}); });
// GET /devices/blackmagic flatten every node's DeckLink cards for the
// recorder picker. Returns one entry per device with the host node info.
router.get('/devices/blackmagic', async (req, res, next) => { router.get('/devices/blackmagic', async (req, res, next) => {
try { try {
const r = await pool.query( const r = await pool.query(
`SELECT id, hostname, ip_address, role, capabilities, `SELECT id, hostname, ip_address, role, capabilities,
EXTRACT(EPOCH FROM (NOW() - last_seen)) AS stale_seconds EXTRACT(EPOCH FROM (NOW() - last_seen)) AS stale_seconds
FROM cluster_nodes FROM cluster_nodes WHERE capabilities IS NOT NULL`
WHERE capabilities IS NOT NULL`
); );
const out = []; const out = [];
for (const row of r.rows) { for (const row of r.rows) {
@ -302,157 +287,98 @@ router.get('/devices/blackmagic', async (req, res, next) => {
const bm = (row.capabilities && row.capabilities.blackmagic) || []; const bm = (row.capabilities && row.capabilities.blackmagic) || [];
const model = (row.capabilities && row.capabilities.blackmagic_model) || null; const model = (row.capabilities && row.capabilities.blackmagic_model) || null;
bm.forEach((d, idx) => { bm.forEach((d, idx) => {
out.push({ out.push({ node_id: row.id, hostname: row.hostname, ip_address: row.ip_address,
node_id: row.id, role: row.role, online, model, index: d.index !== undefined ? d.index : idx, device: d.device });
hostname: row.hostname,
ip_address: row.ip_address,
role: row.role,
online,
model,
index: d.index !== undefined ? d.index : idx,
device: d.device,
});
}); });
} }
res.json(out); res.json(out);
} catch (err) { next(err); } } catch (err) { next(err); }
}); });
// GET /devices/deltacast flatten every node's Deltacast cards for the
// recorder picker. Mirrors /devices/blackmagic shape so the UI can treat
// both card types uniformly.
router.get('/devices/deltacast', async (req, res, next) => { router.get('/devices/deltacast', async (req, res, next) => {
try { try {
const r = await pool.query( const r = await pool.query(
`SELECT id, hostname, ip_address, role, capabilities, `SELECT id, hostname, ip_address, role, capabilities,
EXTRACT(EPOCH FROM (NOW() - last_seen)) AS stale_seconds EXTRACT(EPOCH FROM (NOW() - last_seen)) AS stale_seconds
FROM cluster_nodes FROM cluster_nodes WHERE capabilities IS NOT NULL`
WHERE capabilities IS NOT NULL`
); );
const out = []; const out = [];
for (const row of r.rows) { for (const row of r.rows) {
const online = Number(row.stale_seconds) < 120; const online = Number(row.stale_seconds) < 120;
const dc = (row.capabilities && row.capabilities.deltacast) || []; const dc = (row.capabilities && row.capabilities.deltacast) || [];
const model = (row.capabilities && row.capabilities.deltacast_model) || null; const model = (row.capabilities && row.capabilities.deltacast_model) || null;
// Also synthesise entries from DELTACAST_PORT_COUNT if no entries reported yet —
// useful for nodes that haven't sent a heartbeat since the agent was updated.
dc.forEach((d, idx) => { dc.forEach((d, idx) => {
out.push({ out.push({ node_id: row.id, hostname: row.hostname, ip_address: row.ip_address,
node_id: row.id, role: row.role, online, model: model || 'Deltacast',
hostname: row.hostname, index: d.index !== undefined ? d.index : idx, device: d.device,
ip_address: row.ip_address, present: d.present !== false, port_count: dc.length });
role: row.role,
online,
model: model || 'Deltacast',
index: d.index !== undefined ? d.index : idx,
device: d.device,
present: d.present !== false,
port_count: dc.length,
});
}); });
} }
res.json(out); res.json(out);
} catch (err) { next(err); } } catch (err) { next(err); }
}); });
// GET /devices/deltacast/signal live signal state for Deltacast ports.
// Same pattern as /devices/blackmagic/signal.
router.get('/devices/deltacast/signal', async (req, res, next) => { router.get('/devices/deltacast/signal', async (req, res, next) => {
try { try {
const [nodesRes, recordersRes] = await Promise.all([ const [nodesRes, recordersRes] = await Promise.all([
pool.query( pool.query(`SELECT id, hostname, ip_address, api_url, capabilities,
`SELECT id, hostname, ip_address, api_url, capabilities,
EXTRACT(EPOCH FROM (NOW() - last_seen)) AS stale_seconds EXTRACT(EPOCH FROM (NOW() - last_seen)) AS stale_seconds
FROM cluster_nodes FROM cluster_nodes WHERE capabilities IS NOT NULL`),
WHERE capabilities IS NOT NULL` pool.query(`SELECT id, node_id, device_index, status, source_type, container_id
), FROM recorders WHERE source_type = 'deltacast'`),
pool.query(
`SELECT id, node_id, device_index, status, source_type, container_id
FROM recorders WHERE source_type = 'deltacast'`
),
]); ]);
const recByNodePort = {}; const recByNodePort = {};
for (const rec of recordersRes.rows) { for (const rec of recordersRes.rows) {
recByNodePort[`${rec.node_id}:${rec.device_index}`] = rec; recByNodePort[`${rec.node_id}:${rec.device_index}`] = rec;
} }
const results = []; const results = [];
const fetchPromises = []; const fetchPromises = [];
for (const node of nodesRes.rows) { for (const node of nodesRes.rows) {
const online = Number(node.stale_seconds) < 120; const online = Number(node.stale_seconds) < 120;
const dc = (node.capabilities && node.capabilities.deltacast) || []; const dc = (node.capabilities && node.capabilities.deltacast) || [];
const model = (node.capabilities && node.capabilities.deltacast_model) || 'Deltacast'; const model = (node.capabilities && node.capabilities.deltacast_model) || 'Deltacast';
for (const port of dc) { for (const port of dc) {
const idx = port.index !== undefined ? port.index : dc.indexOf(port); const idx = port.index !== undefined ? port.index : dc.indexOf(port);
const rec = recByNodePort[`${node.id}:${idx}`]; const rec = recByNodePort[`${node.id}:${idx}`];
const base = { const base = { node_id: node.id, hostname: node.hostname, ip_address: node.ip_address,
node_id: node.id, online, model, index: idx, device: port.device, present: port.present !== false,
hostname: node.hostname, recorder_id: rec ? rec.id : null, recorder_status: rec ? rec.status : null,
ip_address: node.ip_address, signal: 'no-recorder', framesReceived: null, currentFps: null };
online,
model,
index: idx,
device: port.device,
present: port.present !== false,
recorder_id: rec ? rec.id : null,
recorder_status: rec ? rec.status : null,
signal: 'no-recorder',
framesReceived: null,
currentFps: null,
};
if (!rec) { results.push(base); continue; } if (!rec) { results.push(base); continue; }
if (rec.status !== 'recording') { base.signal = 'idle'; results.push(base); continue; } if (rec.status !== 'recording') { base.signal = 'idle'; results.push(base); continue; }
// Active recording — query capture container for real signal.
const fetchIdx = results.length; const fetchIdx = results.length;
results.push(base); results.push(base);
fetchPromises.push((async () => { fetchPromises.push((async () => {
try { try {
const url = node.api_url const url = node.api_url ? `${node.api_url}/sidecar/${rec.container_id}/status`
? `${node.api_url}/sidecar/${rec.container_id}/status`
: `http://recorder-${rec.id}:3001/capture/status`; : `http://recorder-${rec.id}:3001/capture/status`;
const r = await fetch(url, { signal: AbortSignal.timeout(2500) }); const r = await fetch(url, { signal: AbortSignal.timeout(2500) });
if (r.ok) { if (r.ok) {
const live = await r.json(); const live = await r.json();
if (live && live.signal) { if (live && live.signal) {
results[fetchIdx].signal = live.signal; results[fetchIdx].signal = live.signal;
results[fetchIdx].framesReceived = live.framesReceived ?? null; results[fetchIdx].framesReceived = live.framesReceived ?? null;
results[fetchIdx].currentFps = live.currentFps ?? null; results[fetchIdx].currentFps = live.currentFps ?? null;
} }
} }
} catch (_) { } catch (_) { results[fetchIdx].signal = 'connecting'; }
results[fetchIdx].signal = 'connecting';
}
})()); })());
} }
} }
await Promise.all(fetchPromises); await Promise.all(fetchPromises);
res.json(results); res.json(results);
} catch (err) { next(err); } } catch (err) { next(err); }
}); });
// GET /:id/ping probe the node's api_url/health endpoint directly
router.get('/:id/ping', async (req, res, next) => { router.get('/:id/ping', async (req, res, next) => {
try { try {
const r = await pool.query( const r = await pool.query('SELECT id, hostname, api_url FROM cluster_nodes WHERE id = $1', [req.params.id]);
'SELECT id, hostname, api_url FROM cluster_nodes WHERE id = $1',
[req.params.id]
);
if (r.rowCount === 0) return res.status(404).json({ error: 'Node not found' }); if (r.rowCount === 0) return res.status(404).json({ error: 'Node not found' });
const node = r.rows[0]; const node = r.rows[0];
if (!node.api_url) return res.json({ reachable: false, reason: 'no api_url registered' }); if (!node.api_url) return res.json({ reachable: false, reason: 'no api_url registered' });
const start = Date.now(); const start = Date.now();
try { try {
const upstream = await fetch(`${node.api_url}/health`, { const upstream = await fetch(`${node.api_url}/health`, { signal: AbortSignal.timeout(4000) });
signal: AbortSignal.timeout(4000),
});
const latency_ms = Date.now() - start; const latency_ms = Date.now() - start;
const body = await upstream.json().catch(() => ({})); const body = await upstream.json().catch(() => ({}));
res.json({ reachable: upstream.ok, latency_ms, status: upstream.status, agent: body }); res.json({ reachable: upstream.ok, latency_ms, status: upstream.status, agent: body });
@ -462,8 +388,83 @@ router.get('/:id/ping', async (req, res, next) => {
} catch (err) { next(err); } } catch (err) { next(err); }
}); });
// ── Capture-driver / SDK deployment ────────────────────────────────────────
// Admins install/update vendor capture-card drivers on a node from the UI.
// We resolve the node's api_url (like /:id/ping) and forward to its node-agent,
// which runs deploy/install-driver.sh <vendor> in a privileged one-shot
// container against the host kernel. Vendor is allowlisted here AND on the
// agent. We never echo the agent token or proprietary paths back to the client.
const DRIVER_VENDORS = ['blackmagic', 'aja', 'deltacast', 'ndi'];
// Bearer the agent expects (its NODE_TOKEN). Configured server-side; never
// derived from client input and never returned to the browser.
function agentAuthHeaders() {
const tok = process.env.NODE_AGENT_TOKEN || '';
return tok ? { Authorization: `Bearer ${tok}` } : {};
}
async function resolveNode(id) {
const r = await pool.query('SELECT id, hostname, api_url, capabilities FROM cluster_nodes WHERE id = $1', [id]);
return r.rowCount === 0 ? null : r.rows[0];
}
router.get('/:id/driver-status', requireAdmin, async (req, res, next) => {
try {
const node = await resolveNode(req.params.id);
if (!node) return res.status(404).json({ error: 'Node not found' });
if (!node.api_url) return res.status(409).json({ error: 'Node has no api_url registered' });
try {
const upstream = await fetch(`${node.api_url}/driver/status`, {
headers: agentAuthHeaders(),
signal: AbortSignal.timeout(6000),
});
const body = await upstream.json().catch(() => ({}));
if (!upstream.ok) {
return res.status(502).json({ error: 'Agent driver-status failed', status: upstream.status });
}
res.json(body);
} catch (err) {
res.status(502).json({ error: 'Node unreachable', reason: err.message });
}
} catch (err) { next(err); }
});
router.post('/:id/install-driver', requireAdmin, async (req, res, next) => {
try {
const vendor = String(req.body?.vendor || '').toLowerCase();
if (!DRIVER_VENDORS.includes(vendor)) {
return res.status(400).json({ error: `Invalid vendor (allowed: ${DRIVER_VENDORS.join(', ')})` });
}
const node = await resolveNode(req.params.id);
if (!node) return res.status(404).json({ error: 'Node not found' });
if (!node.api_url) return res.status(409).json({ error: 'Node has no api_url registered' });
try {
// DKMS builds can take minutes — generous timeout.
const upstream = await fetch(`${node.api_url}/driver/install`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...agentAuthHeaders() },
body: JSON.stringify({ vendor }),
signal: AbortSignal.timeout(600000),
});
const body = await upstream.json().catch(() => ({}));
// Relay logs/result. install-driver.sh never echoes secrets; the agent
// returns only its structured [install-driver] log lines + status.
res.status(upstream.ok ? 200 : 502).json({
ok: !!body.ok,
vendor,
exitCode: body.exitCode ?? null,
rebootRequired: !!body.rebootRequired,
status: body.status ?? null,
logs: typeof body.logs === 'string' ? body.logs : '',
error: body.ok ? undefined : (body.error || 'Install failed — see logs'),
});
} catch (err) {
res.status(502).json({ error: 'Node unreachable or install timed out', reason: err.message });
}
} catch (err) { next(err); }
});
// GET /metrics - live per-node utilization (CPU, RAM, GPU)
router.get('/metrics', async (req, res, next) => { router.get('/metrics', async (req, res, next) => {
try { try {
const r = await pool.query( const r = await pool.query(
@ -471,59 +472,37 @@ router.get('/metrics', async (req, res, next) => {
cpu_usage, mem_used_mb, mem_total_mb, cpu_usage, mem_used_mb, mem_total_mb,
capabilities, metrics, capabilities, metrics,
EXTRACT(EPOCH FROM (NOW() - last_seen)) AS stale_seconds EXTRACT(EPOCH FROM (NOW() - last_seen)) AS stale_seconds
FROM cluster_nodes FROM cluster_nodes ORDER BY registered_at ASC`
ORDER BY registered_at ASC`
); );
const nodes = r.rows.map(row => { const nodes = r.rows.map(row => {
const capGpus = (row.capabilities && row.capabilities.gpus) || []; const capGpus = (row.capabilities && row.capabilities.gpus) || [];
const liveGpus = (row.metrics && row.metrics.gpus) || []; const liveGpus = (row.metrics && row.metrics.gpus) || [];
const gpus = capGpus.map((g, idx) => { const gpus = capGpus.map((g, idx) => {
const live = liveGpus.find(l => l.index === g.index) || liveGpus[idx] || {}; const live = liveGpus.find(l => l.index === g.index) || liveGpus[idx] || {};
return { return { name: g.name || null, util_pct: live.util_pct != null ? live.util_pct : null,
name: g.name || null, memory_used_mb: live.memory_used_mb != null ? live.memory_used_mb : null,
util_pct: live.util_pct != null ? live.util_pct : null, memory_total_mb: g.memory_mb != null ? g.memory_mb : (live.memory_total_mb ?? null) };
memory_used_mb: live.memory_used_mb != null ? live.memory_used_mb : null,
memory_total_mb: g.memory_mb != null ? g.memory_mb : (live.memory_total_mb ?? null),
};
}); });
// include any live GPUs not in static capabilities
for (const lg of liveGpus) { for (const lg of liveGpus) {
if (!capGpus.some(g => g.index === lg.index)) { if (!capGpus.some(g => g.index === lg.index)) {
gpus.push({ gpus.push({ name: lg.name || null, util_pct: lg.util_pct != null ? lg.util_pct : null,
name: lg.name || null, memory_used_mb: lg.memory_used_mb != null ? lg.memory_used_mb : null,
util_pct: lg.util_pct != null ? lg.util_pct : null, memory_total_mb: lg.memory_total_mb != null ? lg.memory_total_mb : null });
memory_used_mb: lg.memory_used_mb != null ? lg.memory_used_mb : null,
memory_total_mb: lg.memory_total_mb != null ? lg.memory_total_mb : null,
});
} }
} }
return { id: row.id, hostname: row.hostname, role: row.role,
return { online: Number(row.stale_seconds) < 120, last_seen: row.last_seen,
id: row.id, cpu_util_pct: row.cpu_usage != null ? Number(row.cpu_usage) : null,
hostname: row.hostname, ram_used_mb: row.mem_used_mb != null ? row.mem_used_mb : null,
role: row.role, ram_total_mb: row.mem_total_mb != null ? row.mem_total_mb : null, gpus };
online: Number(row.stale_seconds) < 120,
last_seen: row.last_seen,
cpu_util_pct: row.cpu_usage != null ? Number(row.cpu_usage) : null,
ram_used_mb: row.mem_used_mb != null ? row.mem_used_mb : null,
ram_total_mb: row.mem_total_mb != null ? row.mem_total_mb : null,
gpus,
};
}); });
res.json({ nodes }); res.json({ nodes });
} catch (err) { next(err); } } catch (err) { next(err); }
}); });
// DELETE /:id deregister a node
router.delete('/:id', async (req, res, next) => { router.delete('/:id', async (req, res, next) => {
try { try {
const r = await pool.query( const r = await pool.query('DELETE FROM cluster_nodes WHERE id = $1 RETURNING id', [req.params.id]);
'DELETE FROM cluster_nodes WHERE id = $1 RETURNING id',
[req.params.id]
);
if (r.rowCount === 0) return res.status(404).json({ error: 'Node not found' }); if (r.rowCount === 0) return res.status(404).json({ error: 'Node not found' });
res.json({ ok: true }); res.json({ ok: true });
} catch (err) { next(err); } } catch (err) { next(err); }

View file

@ -21,7 +21,6 @@ import {
const router = express.Router(); const router = express.Router();
// ── BullMQ: media staging queue (S3 -> /media volume) ────────────────────────
const parseRedisUrl = (url) => { const parseRedisUrl = (url) => {
const parsed = new URL(url); const parsed = new URL(url);
return { host: parsed.hostname, port: parseInt(parsed.port, 10) }; return { host: parsed.hostname, port: parseInt(parsed.port, 10) };
@ -30,7 +29,6 @@ const stageQueue = new Queue('playout-stage', {
connection: parseRedisUrl(process.env.REDIS_URL || 'redis://queue:6379'), connection: parseRedisUrl(process.env.REDIS_URL || 'redis://queue:6379'),
}); });
// ── Sidecar orchestration (mirrors recorders.js) ─────────────────────────────
const PLAYOUT_SIDECAR_IMAGE = process.env.PLAYOUT_IMAGE || 'wild-dragon-playout:latest'; const PLAYOUT_SIDECAR_IMAGE = process.env.PLAYOUT_IMAGE || 'wild-dragon-playout:latest';
function dockerApi(method, path, body = null) { function dockerApi(method, path, body = null) {
@ -68,16 +66,10 @@ async function resolveNodeTarget(nodeId) {
return { remote: true, apiUrl: node.api_url, ip: node.ip_address }; return { remote: true, apiUrl: node.api_url, ip: node.ip_address };
} }
// The sidecar shim listens on this port inside the container. The mam-api talks
// to it by container alias on the shared docker network (local) or via the
// node-agent's returned host:port (remote).
const SIDECAR_HTTP_PORT = 3002; const SIDECAR_HTTP_PORT = 3002;
function channelAlias(id) { return `playout-${id}`; } function channelAlias(id) { return `playout-${id}`; }
// Resolve the base URL the API uses to reach a running channel's sidecar shim.
// Local: the docker-network alias. Remote: the node-agent reported the host the
// container is published on (stored in container_meta.sidecar_url).
function sidecarBaseUrl(channel) { function sidecarBaseUrl(channel) {
if (channel.container_meta && channel.container_meta.sidecar_url) { if (channel.container_meta && channel.container_meta.sidecar_url) {
return channel.container_meta.sidecar_url; return channel.container_meta.sidecar_url;
@ -100,7 +92,6 @@ async function callSidecar(channel, path, method = 'POST', body = null) {
return res.json().catch(() => ({})); return res.json().catch(() => ({}));
} }
// ── Serialization ────────────────────────────────────────────────────────────
function channelToJson(r) { function channelToJson(r) {
return { return {
id: r.id, id: r.id,
@ -123,7 +114,6 @@ function channelToJson(r) {
const OUTPUT_TYPES = new Set(['decklink', 'ndi', 'srt', 'rtmp']); const OUTPUT_TYPES = new Set(['decklink', 'ndi', 'srt', 'rtmp']);
// ── Param resolver: scope every /:id route to the channel's project ──────────
router.param('id', async (req, res, next) => { router.param('id', async (req, res, next) => {
validateUuid('id')(req, res, () => {}); validateUuid('id')(req, res, () => {});
if (res.headersSent) return; if (res.headersSent) return;
@ -143,9 +133,6 @@ async function requireChannelEdit(req, res, next) {
catch (err) { next(err); } catch (err) { next(err); }
} }
// ── Channels ─────────────────────────────────────────────────────────────────
// GET /playout/channels — list (filtered to accessible projects)
router.get('/channels', async (req, res, next) => { router.get('/channels', async (req, res, next) => {
try { try {
let rows; let rows;
@ -162,7 +149,6 @@ router.get('/channels', async (req, res, next) => {
} catch (err) { next(err); } } catch (err) { next(err); }
}); });
// POST /playout/channels — create
router.post('/channels', async (req, res, next) => { router.post('/channels', async (req, res, next) => {
try { try {
const { name, node_id = null, output_type = 'srt', output_config = {}, const { name, node_id = null, output_type = 'srt', output_config = {},
@ -173,8 +159,6 @@ router.post('/channels', async (req, res, next) => {
if (!OUTPUT_TYPES.has(output_type)) { if (!OUTPUT_TYPES.has(output_type)) {
return res.status(400).json({ error: `output_type must be one of: ${[...OUTPUT_TYPES].join(', ')}` }); return res.status(400).json({ error: `output_type must be one of: ${[...OUTPUT_TYPES].join(', ')}` });
} }
// Creating a project-scoped channel requires edit on that project; a
// null-project (admin-only) channel requires admin.
if (project_id) await assertProjectAccess(req.user, project_id, 'edit'); if (project_id) await assertProjectAccess(req.user, project_id, 'edit');
else if (!isAdmin(req.user)) return res.status(403).json({ error: 'admin required for unassigned channel' }); else if (!isAdmin(req.user)) return res.status(403).json({ error: 'admin required for unassigned channel' });
@ -187,7 +171,6 @@ router.post('/channels', async (req, res, next) => {
} catch (err) { next(err); } } catch (err) { next(err); }
}); });
// PATCH /playout/channels/:id — update config (only while stopped)
router.patch('/channels/:id', requireChannelEdit, async (req, res, next) => { router.patch('/channels/:id', requireChannelEdit, async (req, res, next) => {
try { try {
if (req.channel.status === 'running') { if (req.channel.status === 'running') {
@ -214,7 +197,6 @@ router.patch('/channels/:id', requireChannelEdit, async (req, res, next) => {
} catch (err) { next(err); } } catch (err) { next(err); }
}); });
// DELETE /playout/channels/:id
router.delete('/channels/:id', requireChannelEdit, async (req, res, next) => { router.delete('/channels/:id', requireChannelEdit, async (req, res, next) => {
try { try {
if (req.channel.status === 'running') { if (req.channel.status === 'running') {
@ -225,14 +207,9 @@ router.delete('/channels/:id', requireChannelEdit, async (req, res, next) => {
} catch (err) { next(err); } } catch (err) { next(err); }
}); });
// ── Port-contention guard (DeckLink) ─────────────────────────────────────────
// A DeckLink device on a node is exclusive: an active recorder OR another active
// channel on the same node+index blocks a new SDI channel. NDI/SRT/RTMP have no
// hardware contention.
async function assertDeckLinkFree(channel) { async function assertDeckLinkFree(channel) {
if (channel.output_type !== 'decklink') return; if (channel.output_type !== 'decklink') return;
const idx = (channel.output_config && channel.output_config.device_index) || 1; const idx = (channel.output_config && channel.output_config.device_index) || 1;
// Another running channel on the same node + device index?
const chan = await pool.query( const chan = await pool.query(
`SELECT id FROM playout_channels `SELECT id FROM playout_channels
WHERE id <> $1 AND node_id IS NOT DISTINCT FROM $2 AND status = 'running' WHERE id <> $1 AND node_id IS NOT DISTINCT FROM $2 AND status = 'running'
@ -242,7 +219,6 @@ async function assertDeckLinkFree(channel) {
if (chan.rows.length > 0) { if (chan.rows.length > 0) {
throw Object.assign(new Error(`DeckLink device ${idx} already in use by another channel on this node`), { httpStatus: 409 }); throw Object.assign(new Error(`DeckLink device ${idx} already in use by another channel on this node`), { httpStatus: 409 });
} }
// An active recorder using the same device index on the same node?
const rec = await pool.query( const rec = await pool.query(
`SELECT id FROM recorders `SELECT id FROM recorders
WHERE node_id IS NOT DISTINCT FROM $1 AND device_index = $2 WHERE node_id IS NOT DISTINCT FROM $1 AND device_index = $2
@ -254,13 +230,6 @@ async function assertDeckLinkFree(channel) {
} }
} }
// Spawn the CasparCG sidecar for a channel and flip it to 'running'. Shared by
// the /start route and the scheduler failover path (restartChannel) so neither
// duplicates the docker/node-agent orchestration. Caller is responsible for the
// pre-flight guards (status check, DeckLink contention) appropriate to its path.
//
// On any spawn failure the channel is left status='error' with a message and an
// Error carrying { httpStatus } is thrown. On success returns the updated row.
async function spawnChannelSidecar(channel) { async function spawnChannelSidecar(channel) {
await pool.query('UPDATE playout_channels SET status = $1, error_message = NULL WHERE id = $2', ['starting', channel.id]); await pool.query('UPDATE playout_channels SET status = $1, error_message = NULL WHERE id = $2', ['starting', channel.id]);
@ -269,8 +238,6 @@ async function spawnChannelSidecar(channel) {
`OUTPUT_CONFIG=${JSON.stringify(channel.output_config || {})}`, `OUTPUT_CONFIG=${JSON.stringify(channel.output_config || {})}`,
`VIDEO_FORMAT=${channel.video_format}`, `VIDEO_FORMAT=${channel.video_format}`,
`PORT=${SIDECAR_HTTP_PORT}`, `PORT=${SIDECAR_HTTP_PORT}`,
// Drives the HLS preview path (/media/live/<channel_id>/index.m3u8) and
// the per-channel resource naming inside the sidecar.
`CHANNEL_ID=${channel.id}`, `CHANNEL_ID=${channel.id}`,
]; ];
@ -301,7 +268,6 @@ async function spawnChannelSidecar(channel) {
} }
const data = await sidecarRes.json(); const data = await sidecarRes.json();
containerId = data.containerId; containerId = data.containerId;
// node-agent returns the reachable host:port the shim is published on.
if (data.sidecarUrl || data.host) { if (data.sidecarUrl || data.host) {
containerMeta.sidecar_url = data.sidecarUrl || `http://${data.host}:${SIDECAR_HTTP_PORT}`; containerMeta.sidecar_url = data.sidecarUrl || `http://${data.host}:${SIDECAR_HTTP_PORT}`;
} }
@ -314,7 +280,10 @@ async function spawnChannelSidecar(channel) {
Image: PLAYOUT_SIDECAR_IMAGE, Image: PLAYOUT_SIDECAR_IMAGE,
Env: env, Env: env,
HostConfig: { HostConfig: {
Privileged: true, // DeckLink SDI needs raw /dev access (privileged). SRT/NDI/RTMP/HLS run
// unprivileged — privileged exposes host GPUs to CasparCG, and the
// missing in-container NVIDIA driver crashes the engine within seconds.
Privileged: channel.output_type === 'decklink',
NetworkMode: dockerNetwork, NetworkMode: dockerNetwork,
Binds: hostBinds, Binds: hostBinds,
}, },
@ -353,7 +322,6 @@ async function spawnChannelSidecar(channel) {
return rows[0]; return rows[0];
} }
// POST /playout/channels/:id/start — spawn the CasparCG sidecar + bring up output
router.post('/channels/:id/start', requireChannelEdit, async (req, res, next) => { router.post('/channels/:id/start', requireChannelEdit, async (req, res, next) => {
try { try {
const channel = req.channel; const channel = req.channel;
@ -369,7 +337,6 @@ router.post('/channels/:id/start', requireChannelEdit, async (req, res, next) =>
} }
}); });
// POST /playout/channels/:id/stop — tear down the sidecar
router.post('/channels/:id/stop', requireChannelEdit, async (req, res, next) => { router.post('/channels/:id/stop', requireChannelEdit, async (req, res, next) => {
try { try {
const channel = req.channel; const channel = req.channel;
@ -394,7 +361,6 @@ router.post('/channels/:id/stop', requireChannelEdit, async (req, res, next) =>
} catch (err) { next(err); } } catch (err) { next(err); }
}); });
// GET /playout/channels/:id/status — live engine status (proxied to sidecar)
router.get('/channels/:id/status', async (req, res, next) => { router.get('/channels/:id/status', async (req, res, next) => {
try { try {
if (req.channel.status !== 'running') { if (req.channel.status !== 'running') {
@ -448,8 +414,6 @@ async function transport(req, res, action, body = null) {
catch (err) { res.status(502).json({ error: err.message }); } catch (err) { res.status(502).json({ error: err.message }); }
} }
// POST /playout/channels/:id/play — resolve the channel's playlist, stage-check,
// and hand the engine the ordered list of ready clips.
router.post('/channels/:id/play', requireChannelEdit, async (req, res, next) => { router.post('/channels/:id/play', requireChannelEdit, async (req, res, next) => {
try { try {
if (req.channel.status !== 'running') { if (req.channel.status !== 'running') {
@ -503,7 +467,6 @@ router.post('/channels/:id/resume', requireChannelEdit, (req, res) => transport(
router.post('/channels/:id/skip', requireChannelEdit, (req, res) => transport(req, res, '/transport/skip')); router.post('/channels/:id/skip', requireChannelEdit, (req, res) => transport(req, res, '/transport/skip'));
router.post('/channels/:id/stop-playback', requireChannelEdit, (req, res) => transport(req, res, '/channel/stop')); router.post('/channels/:id/stop-playback', requireChannelEdit, (req, res) => transport(req, res, '/channel/stop'));
// GET /playout/channels/:id/asrun — as-run log
router.get('/channels/:id/asrun', async (req, res, next) => { router.get('/channels/:id/asrun', async (req, res, next) => {
try { try {
const { rows } = await pool.query( const { rows } = await pool.query(
@ -513,10 +476,118 @@ router.get('/channels/:id/asrun', async (req, res, next) => {
} catch (err) { next(err); } } catch (err) { next(err); }
}); });
// ── Playlists ──────────────────────────────────────────────────────────────── // ── SCTE-35 ad-break splices ───────────────────────────────────────────────
// Schedule, trigger, and list SCTE-35 ad breaks on a channel. A break can be
// scheduled (after a playlist position, or at a wall-clock time) or fired
// immediately. Firing tells the sidecar to splice the live output, marks the
// break 'fired', and stamps a row in the as-run compliance log.
const SCTE_TYPES = new Set(['splice_insert', 'immediate', 'splice_out', 'splice_in']);
// Fire a break row on the sidecar + record it. Shared by the immediate-trigger
// route and the scheduler's due-break sweep. Best-effort: a sidecar failure
// marks the break 'error' via error_message but never throws to the caller's
// HTTP path beyond what's handled here.
export async function fireScteBreak(channel, brk) {
const out = await callSidecar(channel, '/scte/trigger', 'POST', {
eventId: brk.event_id,
type: brk.type === 'immediate' ? 'splice_insert' : brk.type,
durationS: brk.duration_s,
});
await pool.query(
`UPDATE playout_scte_breaks SET status = 'fired', fired_at = NOW(), updated_at = NOW() WHERE id = $1`,
[brk.id]
);
// Stamp the compliance log. ended_at/duration are known up front for a
// fixed-duration break, so the row is written closed.
await pool.query(
`INSERT INTO playout_as_run
(channel_id, item_id, clip_name, started_at, ended_at, duration_s, result)
VALUES ($1, $2, $3, NOW(),
CASE WHEN $4 > 0 THEN NOW() + ($4 || ' seconds')::interval ELSE NULL END,
$4, 'scte')`,
[channel.id, brk.id, `SCTE-35 ${brk.type} (${brk.duration_s}s)`, brk.duration_s]
);
return out;
}
router.get('/channels/:id/scte', async (req, res, next) => {
try {
const { rows } = await pool.query(
`SELECT * FROM playout_scte_breaks WHERE channel_id = $1 ORDER BY created_at DESC LIMIT 200`,
[req.channel.id]);
res.json(rows);
} catch (err) { next(err); }
});
// Schedule a break. Body: { type, duration_s, playlist_pos?, scheduled_at? }.
// A pending break with a playlist_pos / scheduled_at is fired later by the
// scheduler; one with neither is fired immediately for convenience.
router.post('/channels/:id/scte', requireChannelEdit, async (req, res, next) => {
try {
const { type = 'splice_insert', duration_s = 30,
playlist_pos = null, scheduled_at = null } = req.body || {};
if (!SCTE_TYPES.has(type)) {
return res.status(400).json({ error: `type must be one of: ${[...SCTE_TYPES].join(', ')}` });
}
const dur = Math.max(0, parseInt(duration_s, 10) || 0);
// Auto-assign a monotonically increasing splice_event_id per channel.
const ev = await pool.query(
`SELECT COALESCE(MAX(event_id), 0) + 1 AS next FROM playout_scte_breaks WHERE channel_id = $1`,
[req.channel.id]);
const eventId = ev.rows[0].next;
const { rows } = await pool.query(
`INSERT INTO playout_scte_breaks
(channel_id, playlist_pos, scheduled_at, duration_s, event_id, type, created_by)
VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING *`,
[req.channel.id, playlist_pos, scheduled_at, dur, eventId, type, req.user?.id || null]);
res.status(201).json(rows[0]);
} catch (err) { next(err); }
});
// Fire an ad break immediately ("splice now"). Body: { type?, duration_s? }.
// Creates the break row and triggers the splice on the live output in one shot.
router.post('/channels/:id/scte/trigger', requireChannelEdit, async (req, res, next) => {
try {
if (req.channel.status !== 'running') {
return res.status(409).json({ error: 'Channel is not running' });
}
const { type = 'immediate', duration_s = 30 } = req.body || {};
if (!SCTE_TYPES.has(type)) {
return res.status(400).json({ error: `type must be one of: ${[...SCTE_TYPES].join(', ')}` });
}
const dur = Math.max(0, parseInt(duration_s, 10) || 0);
const ev = await pool.query(
`SELECT COALESCE(MAX(event_id), 0) + 1 AS next FROM playout_scte_breaks WHERE channel_id = $1`,
[req.channel.id]);
const eventId = ev.rows[0].next;
const { rows } = await pool.query(
`INSERT INTO playout_scte_breaks (channel_id, duration_s, event_id, type, status, created_by)
VALUES ($1,$2,$3,$4,'pending',$5) RETURNING *`,
[req.channel.id, dur, eventId, type, req.user?.id || null]);
try {
const out = await fireScteBreak(req.channel, rows[0]);
const updated = await pool.query('SELECT * FROM playout_scte_breaks WHERE id = $1', [rows[0].id]);
res.json({ break: updated.rows[0], engine: out });
} catch (err) {
await pool.query(`UPDATE playout_scte_breaks SET status = 'cancelled', updated_at = NOW() WHERE id = $1`,
[rows[0].id]).catch(() => {});
return res.status(502).json({ error: 'Sidecar unreachable: ' + err.message });
}
} catch (err) { next(err); }
});
router.delete('/channels/:id/scte/:scteId', requireChannelEdit, async (req, res, next) => {
try {
const { rows } = await pool.query(
`UPDATE playout_scte_breaks SET status = 'cancelled', updated_at = NOW()
WHERE id = $1 AND channel_id = $2 AND status = 'pending' RETURNING id`,
[req.params.scteId, req.channel.id]);
if (rows.length === 0) return res.status(404).json({ error: 'Pending break not found' });
res.json({ cancelled: true });
} catch (err) { next(err); }
});
async function loadChannelForBody(req, res, next) { async function loadChannelForBody(req, res, next) {
// For playlist/item routes the channel is referenced indirectly; resolve it
// and assert edit. Used on create/mutate routes that carry channel_id.
const channelId = req.body.channel_id || req.query.channel_id; const channelId = req.body.channel_id || req.query.channel_id;
if (!channelId) return res.status(400).json({ error: 'channel_id is required' }); if (!channelId) return res.status(400).json({ error: 'channel_id is required' });
try { try {
@ -528,7 +599,6 @@ async function loadChannelForBody(req, res, next) {
} catch (err) { next(err); } } catch (err) { next(err); }
} }
// GET /playout/playlists?channel_id=...
router.get('/playlists', async (req, res, next) => { router.get('/playlists', async (req, res, next) => {
try { try {
const channelId = req.query.channel_id; const channelId = req.query.channel_id;
@ -542,7 +612,6 @@ router.get('/playlists', async (req, res, next) => {
} catch (err) { next(err); } } catch (err) { next(err); }
}); });
// POST /playout/playlists
router.post('/playlists', loadChannelForBody, async (req, res, next) => { router.post('/playlists', loadChannelForBody, async (req, res, next) => {
try { try {
const { name, loop = false } = req.body || {}; const { name, loop = false } = req.body || {};
@ -554,7 +623,6 @@ router.post('/playlists', loadChannelForBody, async (req, res, next) => {
} catch (err) { next(err); } } catch (err) { next(err); }
}); });
// GET /playout/playlists/:plid/items
router.get('/playlists/:plid/items', validateUuid('plid'), async (req, res, next) => { router.get('/playlists/:plid/items', validateUuid('plid'), async (req, res, next) => {
try { try {
const pl = await pool.query( const pl = await pool.query(
@ -570,7 +638,6 @@ router.get('/playlists/:plid/items', validateUuid('plid'), async (req, res, next
} catch (err) { next(err); } } catch (err) { next(err); }
}); });
// Helper: load a playlist + assert edit on its channel's project.
async function loadPlaylistEdit(plid, user) { async function loadPlaylistEdit(plid, user) {
const pl = await pool.query( const pl = await pool.query(
`SELECT p.*, c.project_id FROM playout_playlists p `SELECT p.*, c.project_id FROM playout_playlists p
@ -580,7 +647,6 @@ async function loadPlaylistEdit(plid, user) {
return pl.rows[0]; return pl.rows[0];
} }
// POST /playout/playlists/:plid/items — add an asset to a playlist
router.post('/playlists/:plid/items', validateUuid('plid'), async (req, res, next) => { router.post('/playlists/:plid/items', validateUuid('plid'), async (req, res, next) => {
try { try {
await loadPlaylistEdit(req.params.plid, req.user); await loadPlaylistEdit(req.params.plid, req.user);
@ -588,7 +654,6 @@ router.post('/playlists/:plid/items', validateUuid('plid'), async (req, res, nex
transition = 'cut', transition_ms = 0 } = req.body || {}; transition = 'cut', transition_ms = 0 } = req.body || {};
if (!asset_id) return res.status(400).json({ error: 'asset_id is required' }); if (!asset_id) return res.status(400).json({ error: 'asset_id is required' });
// Append at the end of the playlist.
const ord = await pool.query( const ord = await pool.query(
'SELECT COALESCE(MAX(sort_order), -1) + 1 AS next FROM playout_items WHERE playlist_id = $1', 'SELECT COALESCE(MAX(sort_order), -1) + 1 AS next FROM playout_items WHERE playlist_id = $1',
[req.params.plid]); [req.params.plid]);
@ -597,8 +662,6 @@ router.post('/playlists/:plid/items', validateUuid('plid'), async (req, res, nex
VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING *`, VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING *`,
[req.params.plid, asset_id, ord.rows[0].next, in_point, out_point, transition, transition_ms]); [req.params.plid, asset_id, ord.rows[0].next, in_point, out_point, transition, transition_ms]);
// Kick staging immediately so the clip is air-ready by the time the operator
// hits play.
await stageQueue.add('stage', { itemId: rows[0].id, assetId: asset_id }).catch((e) => await stageQueue.add('stage', { itemId: rows[0].id, assetId: asset_id }).catch((e) =>
console.error('[playout] failed to enqueue stage job:', e.message)); console.error('[playout] failed to enqueue stage job:', e.message));
@ -609,7 +672,6 @@ router.post('/playlists/:plid/items', validateUuid('plid'), async (req, res, nex
} }
}); });
// PUT /playout/playlists/:plid/reorder — body { order: [itemId, itemId, ...] }
router.put('/playlists/:plid/reorder', validateUuid('plid'), async (req, res, next) => { router.put('/playlists/:plid/reorder', validateUuid('plid'), async (req, res, next) => {
const client = await pool.connect(); const client = await pool.connect();
try { try {
@ -631,7 +693,6 @@ router.put('/playlists/:plid/reorder', validateUuid('plid'), async (req, res, ne
} finally { client.release(); } } finally { client.release(); }
}); });
// DELETE /playout/items/:itemId
router.delete('/items/:itemId', validateUuid('itemId'), async (req, res, next) => { router.delete('/items/:itemId', validateUuid('itemId'), async (req, res, next) => {
try { try {
const it = await pool.query( const it = await pool.query(
@ -645,7 +706,6 @@ router.delete('/items/:itemId', validateUuid('itemId'), async (req, res, next) =
} catch (err) { next(err); } } catch (err) { next(err); }
}); });
// POST /playout/items/:itemId/stage — (re)kick staging for one item
router.post('/items/:itemId/stage', validateUuid('itemId'), async (req, res, next) => { router.post('/items/:itemId/stage', validateUuid('itemId'), async (req, res, next) => {
try { try {
const it = await pool.query( const it = await pool.query(
@ -660,13 +720,6 @@ router.post('/items/:itemId/stage', validateUuid('itemId'), async (req, res, nex
} catch (err) { next(err); } } catch (err) { next(err); }
}); });
// ── Failover (called by scheduler tick) ──────────────────────────────────────
// Tear down a (presumed dead) sidecar and re-spawn it on another cluster node
// matching the original capability. DeckLink channels are excluded — the
// device-index pinning makes blind re-placement risky, so they alert only.
//
// Returns { restarted: true, new_node_id } on success, or { restarted: false,
// reason } when no eligible node exists or the channel is decklink.
export async function restartChannel(channelId) { export async function restartChannel(channelId) {
const { rows } = await pool.query('SELECT * FROM playout_channels WHERE id = $1', [channelId]); const { rows } = await pool.query('SELECT * FROM playout_channels WHERE id = $1', [channelId]);
if (rows.length === 0) return { restarted: false, reason: 'channel not found' }; if (rows.length === 0) return { restarted: false, reason: 'channel not found' };
@ -676,7 +729,6 @@ export async function restartChannel(channelId) {
return { restarted: false, reason: 'decklink channels are alert-only' }; return { restarted: false, reason: 'decklink channels are alert-only' };
} }
// Best-effort teardown of the old container — it may already be dead.
if (channel.container_id) { if (channel.container_id) {
const { remote, apiUrl } = await resolveNodeTarget(channel.node_id); const { remote, apiUrl } = await resolveNodeTarget(channel.node_id);
if (remote && apiUrl) { if (remote && apiUrl) {
@ -690,9 +742,6 @@ export async function restartChannel(channelId) {
} }
} }
// Pick a different healthy node. For NDI/SRT/RTMP every online node is
// eligible (no hardware contention). Prefer the original if it's still
// online — the failure may have been transient.
const nodes = await pool.query( const nodes = await pool.query(
`SELECT id, hostname, api_url, last_seen_at FROM cluster_nodes `SELECT id, hostname, api_url, last_seen_at FROM cluster_nodes
WHERE id <> $1 AND last_seen_at > NOW() - INTERVAL '60 seconds' WHERE id <> $1 AND last_seen_at > NOW() - INTERVAL '60 seconds'
@ -708,9 +757,6 @@ export async function restartChannel(channelId) {
} }
const newNodeId = nodes.rows[0].id; const newNodeId = nodes.rows[0].id;
// Move the channel to the new node + bump the restart counters; the operator
// UI surfaces these to flag restarts. container_meta is cleared so the new
// spawn re-derives the sidecar URL.
const { rows: moved } = await pool.query( const { rows: moved } = await pool.query(
`UPDATE playout_channels `UPDATE playout_channels
SET node_id = $1, status = 'stopped', container_id = NULL, container_meta = '{}'::jsonb, SET node_id = $1, status = 'stopped', container_id = NULL, container_meta = '{}'::jsonb,
@ -720,10 +766,6 @@ export async function restartChannel(channelId) {
[newNodeId, channel.id] [newNodeId, channel.id]
); );
// Spawn the sidecar directly via the shared helper. We do NOT route through
// the HTTP /start endpoint: its guard rejects status 'starting'/'running' and
// would deadlock the failover. spawnChannelSidecar flips the channel to
// running (or leaves it 'error' and throws on spawn failure).
try { try {
await spawnChannelSidecar(moved[0]); await spawnChannelSidecar(moved[0]);
return { restarted: true, new_node_id: newNodeId }; return { restarted: true, new_node_id: newNodeId };

View file

@ -165,6 +165,94 @@ function pickRecorderFields(body) {
return out; return out;
} }
// Codecs that require an NVIDIA GPU on the target node.
const GPU_CODECS = ['hevc_nvenc', 'h264_nvenc'];
// Issue #163 — codec/container/audio compatibility guard. Returns null when the
// config is valid, otherwise a descriptive error string naming the bad combo.
// `nodeHasGpu` is tri-state: true (GPU present), false (no GPU), or null
// (unknown — node not resolvable at this point, so GPU is only a soft check).
//
// Rules:
// - PCM audio is only valid in MOV/MXF containers, never MP4 (an MP4 with a
// PCM track produces a corrupt/unplayable master — also part of #162).
// - HEVC is not valid in MXF in this build.
// - NVENC codecs require the target node to have a GPU.
function validateRecorderConfig(cfg, nodeHasGpu = null) {
if (!cfg) return null;
const container = String(cfg.recording_container || '').toLowerCase();
const codec = String(cfg.recording_codec || '').toLowerCase();
const audio = String(cfg.recording_audio_codec || '').toLowerCase();
// PCM audio + MP4 → reject.
if (container === 'mp4' && audio.startsWith('pcm')) {
return `Invalid combo: PCM audio (${cfg.recording_audio_codec}) is not supported in an MP4 container. Use a MOV or MXF container, or switch the audio codec to AAC.`;
}
// HEVC in MXF → reject.
if (container === 'mxf' && (codec === 'hevc' || codec === 'hevc_nvenc')) {
return `Invalid combo: HEVC (${cfg.recording_codec}) is not supported in an MXF container in this build. Use a MOV/MP4 container, or pick a DNxHR/ProRes codec for MXF.`;
}
// NVENC requires a GPU on the target node. Only a hard error when we know the
// node lacks one; unknown capability is left as a soft pass.
if (GPU_CODECS.includes(codec) && nodeHasGpu === false) {
return `Invalid combo: codec ${cfg.recording_codec} requires an NVIDIA GPU, but the target node reports no GPU. Choose a software codec (e.g. prores_hq, dnxhr_hq, h264) or assign a GPU node.`;
}
return null;
}
// Resolve whether a recorder's target node has a GPU. Returns true/false when
// the node's heartbeat capability is known, or null when it can't be resolved
// (no node assigned / no capability reported) — callers treat null as a soft
// check per validateRecorderConfig.
async function nodeHasGpuCapability(nodeId) {
if (!nodeId) return null;
try {
const r = await pool.query(
'SELECT capabilities FROM cluster_nodes WHERE id = $1',
[nodeId]
);
if (r.rows.length === 0) return null;
const caps = r.rows[0].capabilities;
const gpus = caps && caps.gpus;
if (!Array.isArray(gpus)) return null;
return gpus.length > 0;
} catch (_) {
return null;
}
}
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
// Issue #162 — after a local-spawn stop, wait for the capture container to
// finalize its master. The asset row was pre-created at start with
// status='live' (display_name = current_session_id); the ingest/finalize step
// flips it to ready/processing once the MOV/MP4 trailer is written. We poll
// until the asset leaves 'live' (or disappears) or we hit the timeout, so we
// don't DELETE the container — and SIGKILL ffmpeg — before the trailer lands.
async function waitForFinalize(recorder, { timeoutMs = 180000, intervalMs = 3000 } = {}) {
if (!recorder.current_session_id) return;
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
try {
const r = await pool.query(
`SELECT 1 FROM assets
WHERE project_id = $1
AND display_name = $2
AND status = 'live'
LIMIT 1`,
[recorder.project_id, recorder.current_session_id]
);
// No live asset row left → finalize is done (or there was none to wait on).
if (r.rows.length === 0) return;
} catch (_) { /* transient DB error — keep polling until timeout */ }
await sleep(intervalMs);
}
}
// GET / - List all recorders // GET / - List all recorders
// //
// Issue #121 — previous version fired N PG queries + N Docker inspects per // Issue #121 — previous version fired N PG queries + N Docker inspects per
@ -255,6 +343,13 @@ router.post('/', async (req, res, next) => {
}; };
const row = { id: uuidv4(), status: 'stopped', ...defaults, ...fields }; const row = { id: uuidv4(), status: 'stopped', ...defaults, ...fields };
// Issue #163 — reject invalid codec/container/audio combos before insert.
const createGpu = await nodeHasGpuCapability(row.node_id);
const createErr = validateRecorderConfig(row, createGpu);
if (createErr) {
return res.status(400).json({ error: createErr });
}
// Build INSERT dynamically so adding columns later means one place to update. // Build INSERT dynamically so adding columns later means one place to update.
const cols = Object.keys(row); const cols = Object.keys(row);
const placeholders = cols.map((_, i) => `$${i + 1}`).join(', '); const placeholders = cols.map((_, i) => `$${i + 1}`).join(', ');
@ -321,6 +416,15 @@ router.patch('/:id', requireRecorderEdit, async (req, res, next) => {
return res.status(400).json({ error: 'No fields to update' }); return res.status(400).json({ error: 'No fields to update' });
} }
// Issue #163 — validate the resulting config (existing row overlaid with the
// incoming changes) so a PATCH can't introduce an invalid combo either.
const merged = { ...recorder, ...fields };
const patchGpu = await nodeHasGpuCapability(merged.node_id);
const patchErr = validateRecorderConfig(merged, patchGpu);
if (patchErr) {
return res.status(400).json({ error: patchErr });
}
const setClause = cols.map((k, i) => `${k} = $${i + 1}`).join(', '); const setClause = cols.map((k, i) => `${k} = $${i + 1}`).join(', ');
const params = cols.map(k => fields[k]); const params = cols.map(k => fields[k]);
params.push(id); params.push(id);
@ -496,11 +600,15 @@ router.post('/:id/start', requireRecorderEdit, async (req, res, next) => {
} }
// GPU-accelerated codecs require the NVIDIA container runtime on the node. // GPU-accelerated codecs require the NVIDIA container runtime on the node.
// hevc_nvenc / h264_nvenc are the only two we currently support; extend // hevc_nvenc / h264_nvenc are the only two we currently support (see the
// this list if av1_nvenc or others are added later. // module-level GPU_CODECS list); extend it if av1_nvenc or others are added.
const GPU_CODECS = ['hevc_nvenc', 'h264_nvenc'];
const useGpu = GPU_CODECS.includes(recorder.recording_codec); const useGpu = GPU_CODECS.includes(recorder.recording_codec);
// Issue #167 — per-recorder GPU affinity. When recorders.gpu_uuid is set the
// sidecar is pinned to that single device (NVIDIA_VISIBLE_DEVICES=<uuid>);
// null keeps the legacy "all" behavior. Only meaningful when useGpu is true.
const gpuUuid = recorder.gpu_uuid || null;
// Determine whether to spawn locally or via a remote node-agent. // Determine whether to spawn locally or via a remote node-agent.
const { remote: isRemote, apiUrl: targetNodeApiUrl } = await resolveNodeTarget(recorder.node_id); const { remote: isRemote, apiUrl: targetNodeApiUrl } = await resolveNodeTarget(recorder.node_id);
// For remote sidecars, the capture container runs on the worker host network and cannot // For remote sidecars, the capture container runs on the worker host network and cannot
@ -518,7 +626,7 @@ router.post('/:id/start', requireRecorderEdit, async (req, res, next) => {
const sidecarRes = await fetch(`${targetNodeApiUrl}/sidecar/start`, { const sidecarRes = await fetch(`${targetNodeApiUrl}/sidecar/start`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ image: 'wild-dragon-capture:latest', env, capturePort, sourceType, useGpu }), body: JSON.stringify({ image: 'wild-dragon-capture:latest', env, capturePort, sourceType, useGpu, gpuUuid }),
signal: AbortSignal.timeout(15000), signal: AbortSignal.timeout(15000),
}); });
if (!sidecarRes.ok) { if (!sidecarRes.ok) {
@ -561,7 +669,8 @@ router.post('/:id/start', requireRecorderEdit, async (req, res, next) => {
const localEnv = [...env]; const localEnv = [...env];
if (useGpu) { if (useGpu) {
localEnv.push('NVIDIA_VISIBLE_DEVICES=all'); // Issue #167 — same per-recorder GPU affinity as the remote sidecar path.
localEnv.push(`NVIDIA_VISIBLE_DEVICES=${gpuUuid || 'all'}`);
localEnv.push('NVIDIA_DRIVER_CAPABILITIES=video,compute,utility'); localEnv.push('NVIDIA_DRIVER_CAPABILITIES=video,compute,utility');
} }
@ -663,9 +772,13 @@ router.post('/:id/stop', requireRecorderEdit, async (req, res, next) => {
return res.status(502).json({ error: 'Remote node failed to stop sidecar' }); return res.status(502).json({ error: 'Remote node failed to stop sidecar' });
} }
} else { } else {
// Issue #162 — stop WITH a grace period (t=180). Docker sends SIGTERM and
// waits up to 180s for ffmpeg to flush and write the MOV/MP4 trailer before
// it SIGKILLs. Without this the master is truncated/corrupt and the
// pre-created asset can get stuck in 'live'.
const stopRes = await dockerApi( const stopRes = await dockerApi(
'POST', 'POST',
`/containers/${recorder.container_id}/stop` `/containers/${recorder.container_id}/stop?t=180`
); );
// 204 = stopped, 304 = already stopped, 404 = container gone — all acceptable. // 204 = stopped, 304 = already stopped, 404 = container gone — all acceptable.
@ -678,6 +791,12 @@ router.post('/:id/stop', requireRecorderEdit, async (req, res, next) => {
// Only attempt remove if the container existed (not 404). // Only attempt remove if the container existed (not 404).
if (stopRes.status !== 404) { if (stopRes.status !== 404) {
// Issue #162 — before removing the container, wait for the master to
// finalize (asset leaves 'live'), mirroring the remote path's reliance on
// the node-agent's clean teardown. This guards against deleting the
// container — and its lingering finalize work — too early.
await waitForFinalize(recorder);
const removeRes = await dockerApi( const removeRes = await dockerApi(
'DELETE', 'DELETE',
`/containers/${recorder.container_id}` `/containers/${recorder.container_id}`

View file

@ -3,10 +3,12 @@
import express from 'express'; import express from 'express';
import pool from '../db/pool.js'; import pool from '../db/pool.js';
import { hashPassword } from '../auth/passwords.js'; import { hashPassword } from '../auth/passwords.js';
import { DEV_USER_ID } from '../middleware/auth.js'; import { DEV_USER_ID, requireAdmin } from '../middleware/auth.js';
import { accessibleProjectIds } from '../auth/authz.js';
const router = express.Router(); const router = express.Router();
const MIN_PASSWORD_LEN = 12; const MIN_PASSWORD_LEN = 12;
const ROLES = ['admin', 'editor', 'viewer'];
function bad(res, msg) { return res.status(400).json({ error: msg }); } function bad(res, msg) { return res.status(400).json({ error: msg }); }
@ -14,7 +16,7 @@ function bad(res, msg) { return res.status(400).json({ error: msg }); }
router.get('/', async (_req, res, next) => { router.get('/', async (_req, res, next) => {
try { try {
const { rows } = await pool.query( const { rows } = await pool.query(
`SELECT id, username, display_name, role, last_login_at, created_at `SELECT id, username, display_name, role, totp_enabled, last_login_at, created_at
FROM users WHERE id <> $1 ORDER BY username`, [DEV_USER_ID]); FROM users WHERE id <> $1 ORDER BY username`, [DEV_USER_ID]);
res.json(rows); res.json(rows);
} catch (err) { next(err); } } catch (err) { next(err); }
@ -26,6 +28,7 @@ router.post('/', async (req, res, next) => {
const { username, password, display_name, role } = req.body || {}; const { username, password, display_name, role } = req.body || {};
if (!username || typeof username !== 'string') return bad(res, 'username required'); if (!username || typeof username !== 'string') return bad(res, 'username required');
if (!password || password.length < MIN_PASSWORD_LEN) return bad(res, 'password must be at least ' + MIN_PASSWORD_LEN + ' chars'); if (!password || password.length < MIN_PASSWORD_LEN) return bad(res, 'password must be at least ' + MIN_PASSWORD_LEN + ' chars');
if (role !== undefined && !ROLES.includes(role)) return bad(res, "role must be one of: " + ROLES.join(', '));
const hash = await hashPassword(password); const hash = await hashPassword(password);
const { rows } = await pool.query( const { rows } = await pool.query(
`INSERT INTO users (username, password_hash, display_name, role) `INSERT INTO users (username, password_hash, display_name, role)
@ -76,7 +79,10 @@ router.patch('/:id', async (req, res, next) => {
if (req.params.id === DEV_USER_ID) return res.status(400).json({ error: 'cannot edit dev user' }); if (req.params.id === DEV_USER_ID) return res.status(400).json({ error: 'cannot edit dev user' });
const sets = []; const vals = []; const sets = []; const vals = [];
if (typeof req.body?.display_name === 'string') { sets.push('display_name = $' + (sets.length + 1)); vals.push(req.body.display_name); } if (typeof req.body?.display_name === 'string') { sets.push('display_name = $' + (sets.length + 1)); vals.push(req.body.display_name); }
if (typeof req.body?.role === 'string') { sets.push('role = $' + (sets.length + 1)); vals.push(req.body.role); } if (typeof req.body?.role === 'string') {
if (!ROLES.includes(req.body.role)) return bad(res, "role must be one of: " + ROLES.join(', '));
sets.push('role = $' + (sets.length + 1)); vals.push(req.body.role);
}
if (typeof req.body?.password === 'string') { if (typeof req.body?.password === 'string') {
if (req.body.password.length < MIN_PASSWORD_LEN) return bad(res, 'password must be at least ' + MIN_PASSWORD_LEN + ' chars'); if (req.body.password.length < MIN_PASSWORD_LEN) return bad(res, 'password must be at least ' + MIN_PASSWORD_LEN + ' chars');
sets.push('password_hash = $' + (sets.length + 1) + ', password_updated_at = NOW()'); sets.push('password_hash = $' + (sets.length + 1) + ', password_updated_at = NOW()');
@ -93,4 +99,88 @@ router.patch('/:id', async (req, res, next) => {
} catch (err) { next(err); } } catch (err) { next(err); }
}); });
// GET /:id/access — effective per-project access for one user (admin only).
// Reuses authz.accessibleProjectIds (MAX over direct user grant + every group the
// user belongs to). `via` is 'direct' for a user grant, 'group:<name>' otherwise.
// When the effective level comes from several sources we report the direct grant
// if present, else the first contributing group.
router.get('/:id/access', requireAdmin, async (req, res, next) => {
try {
const { rows: urows } = await pool.query(
`SELECT id, role FROM users WHERE id = $1`, [req.params.id]);
if (urows.length === 0) return res.status(404).json({ error: 'user not found' });
const target = urows[0];
const { rows: groups } = await pool.query(
`SELECT g.id, g.name
FROM user_groups ug JOIN groups g ON g.id = ug.group_id
WHERE ug.user_id = $1 ORDER BY g.name`, [target.id]);
// Admins bypass scoping — every project at 'edit', via their role.
const access = await accessibleProjectIds(target);
if (access.all) {
const { rows: projects } = await pool.query(
`SELECT id, name FROM projects ORDER BY name`);
return res.json({
projects: projects.map(p => ({
project_id: p.id, project_name: p.name, level: 'edit', via: 'direct',
})),
groups,
});
}
const ids = [...access.ids];
if (ids.length === 0) return res.json({ projects: [], groups });
// Resolve names + the source of each grant. groupNameById lets us label a
// group-sourced grant; a direct user grant always wins the `via` label.
const groupNameById = new Map(groups.map(g => [g.id, g.name]));
const { rows: grants } = await pool.query(
`SELECT pa.project_id, pa.subject_type, pa.subject_id, pa.level, p.name AS project_name
FROM project_access pa JOIN projects p ON p.id = pa.project_id
WHERE (pa.subject_type = 'user' AND pa.subject_id = $1)
OR (pa.subject_type = 'group' AND pa.subject_id IN (
SELECT group_id FROM user_groups WHERE user_id = $1
))`,
[target.id]);
const byProject = new Map();
for (const g of grants) {
const eff = access.levelByProject.get(g.project_id); // already the MAX
const via = g.subject_type === 'user'
? 'direct'
: 'group:' + (groupNameById.get(g.subject_id) || g.subject_id);
const prev = byProject.get(g.project_id);
// Keep a row only if it carries the effective level; prefer a direct grant
// when both a direct and a group grant hit the same level.
if (g.level === eff && (!prev || (prev.via !== 'direct' && via === 'direct'))) {
byProject.set(g.project_id, {
project_id: g.project_id, project_name: g.project_name, level: eff, via,
});
}
}
res.json({
projects: [...byProject.values()].sort((a, b) => a.project_name.localeCompare(b.project_name)),
groups,
});
} catch (err) { next(err); }
});
// POST /:id/totp/disable — admin clears a locked-out user's 2FA WITHOUT their
// password (the self-service /auth/totp/disable needs the victim's own). Mirrors
// that handler's SQL but targets :id and skips the password check. Dev user blocked.
router.post('/:id/totp/disable', requireAdmin, async (req, res, next) => {
try {
if (req.params.id === DEV_USER_ID) return res.status(400).json({ error: 'cannot edit dev user' });
const { rowCount } = await pool.query(
`UPDATE users SET totp_enabled = FALSE, totp_secret = NULL, totp_last_counter = 0
WHERE id = $1 AND id <> $2`,
[req.params.id, DEV_USER_ID]);
if (rowCount === 0) return res.status(404).json({ error: 'user not found' });
await pool.query(`DELETE FROM user_recovery_codes WHERE user_id = $1`, [req.params.id]);
res.status(204).end();
} catch (err) { next(err); }
});
export default router; export default router;

View file

@ -9,7 +9,7 @@
import pool from './db/pool.js'; import pool from './db/pool.js';
import { syncToAmpp } from './routes/upload.js'; import { syncToAmpp } from './routes/upload.js';
import { restartChannel } from './routes/playout.js'; import { restartChannel, fireScteBreak } from './routes/playout.js';
import { INTERNAL_TOKEN } from './middleware/auth.js'; import { INTERNAL_TOKEN } from './middleware/auth.js';
const TICK_INTERVAL_MS = parseInt(process.env.SCHEDULER_TICK_MS || '15000', 10); const TICK_INTERVAL_MS = parseInt(process.env.SCHEDULER_TICK_MS || '15000', 10);
@ -34,11 +34,7 @@ async function callSelf(path, method = 'POST') {
return res.json().catch(() => ({})); return res.json().catch(() => ({}));
} }
// Issue #103 — every mam-api replica runs the same tick on the same interval, const SCHEDULER_LOCK_KEY = 8210301;
// so a multi-node deploy would double-fire recorder starts/stops. We guard
// the whole tick with a PG advisory lock (1 = scheduler) so exactly one
// replica processes a given interval. Pure-Postgres, no extra infra.
const SCHEDULER_LOCK_KEY = 8210301; // arbitrary, must be stable across replicas
async function tryAcquireSchedulerLock(client) { async function tryAcquireSchedulerLock(client) {
const r = await client.query('SELECT pg_try_advisory_lock($1) AS got', [SCHEDULER_LOCK_KEY]); const r = await client.query('SELECT pg_try_advisory_lock($1) AS got', [SCHEDULER_LOCK_KEY]);
@ -57,14 +53,9 @@ async function tick() {
try { try {
haveLock = await tryAcquireSchedulerLock(client); haveLock = await tryAcquireSchedulerLock(client);
if (!haveLock) { if (!haveLock) {
// Another replica is processing this interval — bail silently.
return; return;
} }
// 1) Atomically claim pending schedules whose window has opened. The
// UPDATE...RETURNING flips status to 'running' in the same statement
// so even if another replica got past the lock (it can't, but
// belt-and-braces) each row can only be claimed once.
const dueStart = await client.query( const dueStart = await client.query(
`UPDATE recorder_schedules `UPDATE recorder_schedules
SET status = 'starting', updated_at = NOW() SET status = 'starting', updated_at = NOW()
@ -97,7 +88,6 @@ async function tick() {
} }
} }
// 2) Atomically claim running schedules whose window has closed.
const dueStop = await client.query( const dueStop = await client.query(
`UPDATE recorder_schedules `UPDATE recorder_schedules
SET status = 'stopping', updated_at = NOW() SET status = 'stopping', updated_at = NOW()
@ -120,7 +110,6 @@ async function tick() {
console.log(`[scheduler] stopped schedule "${s.name}" on recorder ${s.recorder_id}`); console.log(`[scheduler] stopped schedule "${s.name}" on recorder ${s.recorder_id}`);
await enqueueNextOccurrence(s, client); await enqueueNextOccurrence(s, client);
} catch (err) { } catch (err) {
// Stop failed — flag as failed but don't keep trying forever.
await client.query( await client.query(
`UPDATE recorder_schedules `UPDATE recorder_schedules
SET status = 'failed', error_message = $2, updated_at = NOW() SET status = 'failed', error_message = $2, updated_at = NOW()
@ -131,7 +120,6 @@ async function tick() {
} }
} }
// 3) If a schedule was cancelled while running, stop the recorder.
const cancelledRunning = await client.query( const cancelledRunning = await client.query(
`SELECT s.* FROM recorder_schedules s `SELECT s.* FROM recorder_schedules s
JOIN recorders r ON r.id = s.recorder_id JOIN recorders r ON r.id = s.recorder_id
@ -147,9 +135,6 @@ async function tick() {
} }
} }
// 4) Mark stale live assets as 'error' (#66).
// If a capture container crashes without calling mark-empty/mark-complete,
// the asset row stays status='live' indefinitely. Timeout after 2 hours.
const LIVE_TIMEOUT_MINUTES = parseInt(process.env.LIVE_ASSET_TIMEOUT_MINUTES || '120', 10); const LIVE_TIMEOUT_MINUTES = parseInt(process.env.LIVE_ASSET_TIMEOUT_MINUTES || '120', 10);
const staleResult = await client.query( const staleResult = await client.query(
`UPDATE assets `UPDATE assets
@ -166,9 +151,6 @@ async function tick() {
} }
} }
// 5) AMPP sync retry (#77). Pick up any pending/failed rows whose
// next-attempt time has arrived and retry them. Cap per tick so we
// don't burn budget on a single rough interval.
const ampps = await client.query( const ampps = await client.query(
`SELECT id, project_id, bin_id FROM assets `SELECT id, project_id, bin_id FROM assets
WHERE ampp_sync_status IN ('pending', 'failed') WHERE ampp_sync_status IN ('pending', 'failed')
@ -181,11 +163,6 @@ async function tick() {
await syncToAmpp(row.id, row.project_id, row.bin_id); await syncToAmpp(row.id, row.project_id, row.bin_id);
} }
// 6) Playout channel health checks. Ping each running channel's sidecar
// /status; on success bump last_heartbeat_at, on failure increment a
// transient miss counter (in playout_sidecars.last_heartbeat_at age).
// Three consecutive misses → auto-restart on a healthy node (non-
// decklink), or alert-only for decklink.
await playoutHealthTick(client); await playoutHealthTick(client);
} catch (err) { } catch (err) {
console.error('[scheduler] tick error:', err); console.error('[scheduler] tick error:', err);
@ -285,7 +262,6 @@ async function playoutHealthTick(client) {
FROM playout_channels WHERE status = 'running'` FROM playout_channels WHERE status = 'running'`
)); ));
} catch (err) { } catch (err) {
// Migration 029 may not be applied yet — bail silently rather than crash.
if (err.code === '42P01') return; if (err.code === '42P01') return;
throw err; throw err;
} }
@ -313,6 +289,32 @@ async function playoutHealthTick(client) {
} catch (e) { } catch (e) {
console.warn(`[scheduler] as-run write failed for ${ch.id}: ${e.message}`); console.warn(`[scheduler] as-run write failed for ${ch.id}: ${e.message}`);
} }
// SCTE-35: fire any pending scheduled breaks now due. Position-based
// breaks (playlist_pos) fire when the engine reaches that item; wall-clock
// breaks fire at scheduled_at. Failures mark the break cancelled so a bad
// break never wedges the sweep.
try {
const { rows: due } = await client.query(
`SELECT * FROM playout_scte_breaks
WHERE channel_id = $1 AND status = 'pending'
AND ( (scheduled_at IS NOT NULL AND scheduled_at <= NOW())
OR (playlist_pos IS NOT NULL AND playlist_pos <= $2) )
ORDER BY created_at ASC`,
[ch.id, (engine && engine.currentIndex >= 0) ? engine.currentIndex : -1]
);
for (const brk of due) {
try { await fireScteBreak(ch, brk); }
catch (e2) {
console.warn(`[scheduler] scte fire failed for break ${brk.id}: ${e2.message}`);
await client.query(
`UPDATE playout_scte_breaks SET status = 'cancelled', updated_at = NOW() WHERE id = $1`,
[brk.id]).catch(() => {});
}
}
} catch (e) {
console.warn(`[scheduler] scte sweep failed for ${ch.id}: ${e.message}`);
}
} catch (err) { } catch (err) {
// When last_heartbeat_at is NULL (channel just spawned), fall back to // When last_heartbeat_at is NULL (channel just spawned), fall back to
// updated_at (set to NOW() by spawnChannelSidecar). This prevents a // updated_at (set to NOW() by spawnChannelSidecar). This prevents a
@ -321,7 +323,7 @@ async function playoutHealthTick(client) {
const baseline = ch.last_heartbeat_at || ch.updated_at; const baseline = ch.last_heartbeat_at || ch.updated_at;
const lastSeen = baseline ? new Date(baseline).getTime() : Date.now(); const lastSeen = baseline ? new Date(baseline).getTime() : Date.now();
const ageMs = Date.now() - lastSeen; const ageMs = Date.now() - lastSeen;
if (ageMs < TIMEOUT_MS) continue; // not yet 3 misses if (ageMs < TIMEOUT_MS) continue;
if (ch.output_type === 'decklink') { if (ch.output_type === 'decklink') {
await client.query( await client.query(
@ -334,8 +336,6 @@ async function playoutHealthTick(client) {
console.warn(`[scheduler] failover: channel ${ch.id} unreachable (${err.message}), restart #${ch.restart_count + 1}`); console.warn(`[scheduler] failover: channel ${ch.id} unreachable (${err.message}), restart #${ch.restart_count + 1}`);
try { try {
// restartChannel re-places the channel on a healthy node AND spawns the
// new sidecar directly (shared helper) — no /start self-call needed.
const res = await restartChannel(ch.id); const res = await restartChannel(ch.id);
if (res.restarted) { if (res.restarted) {
console.log(`[scheduler] failover: channel ${ch.id} re-placed on node ${res.new_node_id}`); console.log(`[scheduler] failover: channel ${ch.id} re-placed on node ${res.new_node_id}`);
@ -352,8 +352,6 @@ async function playoutHealthTick(client) {
export function startSchedulerLoop() { export function startSchedulerLoop() {
if (_interval) return; if (_interval) return;
console.log(`[scheduler] tick loop started (interval=${TICK_INTERVAL_MS}ms)`); console.log(`[scheduler] tick loop started (interval=${TICK_INTERVAL_MS}ms)`);
// Fire once on startup so a window that opened while the API was down
// doesn't have to wait a full interval.
setTimeout(() => tick().catch(() => {}), 2000); setTimeout(() => tick().catch(() => {}), 2000);
_interval = setInterval(() => tick().catch(() => {}), TICK_INTERVAL_MS); _interval = setInterval(() => tick().catch(() => {}), TICK_INTERVAL_MS);
} }

View file

@ -1,14 +1,142 @@
import http from 'http'; import http from 'http';
import os from 'os'; import os from 'os';
import fs from 'fs'; import fs from 'fs';
import { spawn } from 'child_process';
const MAM_API_URL = (process.env.MAM_API_URL || 'http://localhost:3000').replace(/\/$/, ''); const MAM_API_URL = (process.env.MAM_API_URL || 'http://localhost:3000').replace(/\/$/, '');
const NODE_TOKEN = process.env.NODE_TOKEN || ''; const NODE_TOKEN = process.env.NODE_TOKEN || '';
const NODE_ROLE = process.env.NODE_ROLE || 'worker'; const NODE_ROLE = process.env.NODE_ROLE || 'worker';
// Cluster identity. The heartbeat keys cluster_nodes on hostname (ON CONFLICT
// (hostname)), so two machines reporting the SAME os.hostname() clobber each
// other's row — exactly what happens with cloned VMs that share /etc/hostname
// (e.g. two boxes both named "zampp1"). The capture node's DeckLink capability
// then lands on the wrong row and gets overwritten by the primary's cardless
// heartbeat, so the recorder UI shows "No SDI devices auto-detected".
// NODE_NAME (set per-node by onboard-node.sh / the node's .env) overrides
// os.hostname() so identity is explicit and collision-proof. Falls back to the
// OS hostname when unset, preserving existing single-host behaviour.
const NODE_NAME = process.env.NODE_NAME || os.hostname();
const AGENT_PORT = parseInt(process.env.AGENT_PORT || '7436', 10); const AGENT_PORT = parseInt(process.env.AGENT_PORT || '7436', 10);
const HEARTBEAT_MS = parseInt(process.env.HEARTBEAT_MS || '30000', 10); const HEARTBEAT_MS = parseInt(process.env.HEARTBEAT_MS || '30000', 10);
const LIVE_DIR = process.env.LIVE_DIR || '/mnt/NVME/MAM/wild-dragon-live'; const LIVE_DIR = process.env.LIVE_DIR || '/mnt/NVME/MAM/wild-dragon-live';
const VERSION = '1.3.0'; // Host path to the checked-out repo (onboard-node.sh clones to /opt/wild-dragon).
// The driver-install container bind-mounts this so install-driver.sh can read
// sdk/<vendor>/ and run from deploy/. Overridable for non-standard layouts.
const REPO_DIR = process.env.REPO_DIR || '/opt/wild-dragon';
const VERSION = '1.4.0';
// Capture-driver vendor allowlist. NOTHING outside this set is ever passed to
// the host installer — the value is only ever used to pick a script arg, never
// interpolated into a shell string.
const DRIVER_VENDORS = ['blackmagic', 'aja', 'deltacast', 'ndi'];
// ── Deltacast board-open mutex (legacy — no longer used) ─────────────────
// The per-sidecar board-open race is eliminated by the shared bridge daemon
// (deltacast-bridge). This mutex is kept but acquireDcLock() is never called
// for deltacast sidecars; they wait for the bridge FIFOs instead.
const DELTACAST_STAGGER_MS = parseInt(process.env.DELTACAST_START_STAGGER_MS || '3500', 10);
let _dcMutex = Promise.resolve();
function acquireDcLock() {
let release;
const next = new Promise(resolve => { release = resolve; });
const wait = _dcMutex;
_dcMutex = _dcMutex.then(() => next);
return wait.then(() => release);
}
// ── Deltacast shared bridge daemon ────────────────────────────────────────
//
// ONE deltacast-bridge process runs on the HOST (not inside a container) and
// opens the board handle exactly once, serving all requested ports via FIFOs
// in /dev/shm/deltacast/. This eliminates the BufMngr.c:781 OOB fault caused
// by concurrent VHD_OpenBoardHandle calls.
//
// Lifecycle:
// - First deltacast sidecar start → bridge launched with all configured ports.
// - Subsequent starts → sidecar reads existing FIFOs; bridge unchanged.
// - Last deltacast sidecar stop → bridge killed.
// - Bridge unexpected exit → _dcBridge reset; next sidecar re-launches it.
//
// DELTACAST_PIPE_DIR (default /dev/shm/deltacast): FIFO directory, bind-mounted
// into each deltacast sidecar so ffmpeg can read the FIFOs.
// DELTACAST_BRIDGE_BIN (default deltacast-bridge): host path to the binary.
// Typically /usr/local/bin/deltacast-bridge after `make install` from the SDK
// build, or set to the build-dir path for development.
// DELTACAST_PORTS (csv, e.g. "0,1,2,4,7"): ports the bridge opens at launch.
// Defaults to all 8 ports (0-7) so any sidecar port combination is covered.
const DC_PIPE_DIR = process.env.DELTACAST_PIPE_DIR || '/dev/shm/deltacast';
const DC_BRIDGE_BIN = process.env.DELTACAST_BRIDGE_BIN || 'deltacast-bridge';
const DC_PORTS_CSV = process.env.DELTACAST_PORTS || '0,1,2,3,4,5,6,7';
const DC_BOARD = process.env.DELTACAST_BOARD || '0';
let _dcBridge = null; // ChildProcess | null
let _dcSidecarCount = 0; // active deltacast sidecars on this node
// Map containerId -> sourceType so stop() can decrement the deltacast counter.
const _containerSourceType = new Map();
// port -> fmt JSON from bridge stderr (inject into sidecar env)
const _dcPortFmt = new Map();
function _dcBridgeRunning() {
return _dcBridge !== null && _dcBridge.exitCode === null && _dcBridge.signalCode === null;
}
function startDeltacastBridge() {
if (_dcBridgeRunning()) return; // already up
try { fs.mkdirSync(DC_PIPE_DIR, { recursive: true }); } catch (_) {}
const _v0 = DC_PIPE_DIR + '/video-0.fifo';
if (fs.existsSync(_v0)) { console.log('[dc-bridge] FIFOs exist, skipping spawn'); return; }
const args = [
'--device', DC_BOARD,
'--ports', DC_PORTS_CSV,
'--video-pipe-dir', DC_PIPE_DIR,
'--audio-pipe-dir', DC_PIPE_DIR,
];
console.log(`[dc-bridge] launching: ${DC_BRIDGE_BIN} ${args.join(' ')}`);
const proc = spawn(DC_BRIDGE_BIN, args, {
stdio: ['ignore', 'ignore', 'pipe'],
detached: false,
});
proc.stderr.setEncoding('utf8');
proc.stderr.on('data', (chunk) => {
for (const line of chunk.split('\n')) {
const t = line.trim();
if (!t) continue;
// Format JSON lines go to stdout so node-agent can log/forward them.
if (t.startsWith('{')) {
console.log('[dc-bridge] ' + t);
try { const f = JSON.parse(t); if (typeof f.port === 'number') _dcPortFmt.set(f.port, f); } catch (_) {}
} else {
console.error('[dc-bridge] ' + t);
}
}
});
proc.on('exit', (code, sig) => {
console.error(`[dc-bridge] exited code=${code} signal=${sig}`);
_dcBridge = null;
});
_dcBridge = proc;
console.log(`[dc-bridge] pid=${proc.pid} board=${DC_BOARD} ports=${DC_PORTS_CSV}`);
}
function stopDeltacastBridge() {
if (!_dcBridgeRunning()) return;
console.log('[dc-bridge] stopping (no active deltacast sidecars)');
try { _dcBridge.kill('SIGTERM'); } catch (_) {}
// Give it 5s to clean up, then SIGKILL.
const proc = _dcBridge;
setTimeout(() => {
try { if (proc.exitCode === null) proc.kill('SIGKILL'); } catch (_) {}
}, 5000);
_dcBridge = null;
}
// Pick the host's LAN IP. Inside a bridge-mode container, // Pick the host's LAN IP. Inside a bridge-mode container,
// os.networkInterfaces() returns the container's docker-bridge IP (172.x), // os.networkInterfaces() returns the container's docker-bridge IP (172.x),
@ -93,6 +221,11 @@ async function handleSidecarStart(body, res) {
// (ProRes, DNxHR, libx264) don't need it and it avoids a hard dep on the // (ProRes, DNxHR, libx264) don't need it and it avoids a hard dep on the
// NVIDIA container runtime on nodes that have no GPU. // NVIDIA container runtime on nodes that have no GPU.
useGpu = false, useGpu = false,
// Issue #167 — optional per-recorder GPU affinity. When set to a GPU
// UUID (e.g. "GPU-xxxx") or a numeric index, the sidecar is pinned to
// that single device via NVIDIA_VISIBLE_DEVICES instead of "all". null /
// undefined keeps the legacy "all" behavior (expose every GPU).
gpuUuid = null,
} = body; } = body;
const binds = [`${LIVE_DIR}:/live`]; const binds = [`${LIVE_DIR}:/live`];
@ -103,17 +236,22 @@ async function handleSidecarStart(body, res) {
try { try {
const dcEntries = fs.readdirSync('/dev').filter(n => /^deltacast\d+$/.test(n)); const dcEntries = fs.readdirSync('/dev').filter(n => /^deltacast\d+$/.test(n));
for (const d of dcEntries) binds.push(`/dev/${d}:/dev/${d}`); for (const d of dcEntries) binds.push(`/dev/${d}:/dev/${d}`);
// VideoMaster SDK needs the board IPC shared-memory segment mounted too.
if (fs.existsSync('/dev/shm/deltacast')) binds.push('/dev/shm/deltacast:/dev/shm/deltacast');
} catch (_) { /* /dev always exists */ } } catch (_) { /* /dev always exists */ }
} }
// Build the sidecar environment, injecting NVIDIA vars when GPU is requested. // Build the sidecar environment, injecting NVIDIA vars when GPU is requested.
const sidecarEnv = [...env, `PORT=${capturePort}`]; const sidecarEnv = [...env, `PORT=${capturePort}`];
if (useGpu) { if (useGpu) {
// NVIDIA_VISIBLE_DEVICES=all exposes every GPU on the host. // Issue #167 — per-recorder GPU affinity. A gpuUuid (UUID string or
// For a single-GPU node (zampp2 / L4) this is equivalent to pinning GPU 0. // numeric index) pins the sidecar to exactly that device; otherwise
// When we later store per-recorder GPU affinity in the DB we can pass a // NVIDIA_VISIBLE_DEVICES=all exposes every GPU on the host (legacy
// specific UUID here instead. // behavior — for a single-GPU node like zampp2 / L4 this equals GPU 0).
sidecarEnv.push('NVIDIA_VISIBLE_DEVICES=all'); const visibleDevices = (gpuUuid != null && String(gpuUuid).trim() !== '')
? String(gpuUuid).trim()
: 'all';
sidecarEnv.push(`NVIDIA_VISIBLE_DEVICES=${visibleDevices}`);
sidecarEnv.push('NVIDIA_DRIVER_CAPABILITIES=video,compute,utility'); sidecarEnv.push('NVIDIA_DRIVER_CAPABILITIES=video,compute,utility');
} }
@ -137,22 +275,60 @@ async function handleSidecarStart(body, res) {
HostConfig: hostConfig, HostConfig: hostConfig,
}; };
const createRes = await dockerApi('POST', '/containers/create', spec); // Deltacast: ensure the shared bridge daemon is running on the HOST before
if (createRes.status !== 201) { // starting the sidecar. The sidecar reads FIFOs produced by the bridge;
return jsonResponse(res, 502, { error: 'Failed to create container', details: createRes.data }); // it does NOT open the board handle itself (no BufMngr.c:781 race).
if (sourceType === 'deltacast') {
_dcSidecarCount++;
startDeltacastBridge();
// Inject per-port signal format so capture-manager uses real dimensions/fps
const _srcCfg = (env.find(e => e.startsWith('SOURCE_CONFIG=')) || '').slice(14);
let _portNum = NaN;
try { _portNum = JSON.parse(_srcCfg).port; } catch (_) {}
if (Number.isFinite(_portNum) && _dcPortFmt.has(_portNum)) {
const _fmt = _dcPortFmt.get(_portNum);
const _fps = (_fmt.fps_den && _fmt.fps_den !== 1) ? `${_fmt.fps_num}/${_fmt.fps_den}` : String(_fmt.fps_num);
sidecarEnv.push(`DELTACAST_VIDEO_SIZE=${_fmt.width}x${_fmt.height}`);
sidecarEnv.push(`DELTACAST_FRAMERATE=${_fps}`);
sidecarEnv.push(`DELTACAST_INTERLACED=${_fmt.interlaced ? '1' : '0'}`);
console.log(`[dc-bridge] port ${_portNum} fmt: ${_fmt.width}x${_fmt.height} ${_fps} interlaced=${_fmt.interlaced}`);
}
} }
const containerId = createRes.data.Id; let containerId;
const _u = (env.find(e => e.startsWith('MAM_API_URL=')) || '').slice(12); try {
const _tok = env.some(e => e.startsWith('MAM_API_TOKEN=') && e.length > 14); const createRes = await dockerApi('POST', '/containers/create', spec);
console.log(`[sidecar-start] ${containerId} image=${image} src=${sourceType} MAM_API_URL=${_u} token=${_tok}`); if (createRes.status !== 201) {
const startRes = await dockerApi('POST', `/containers/${containerId}/start`); if (sourceType === 'deltacast') {
if (startRes.status !== 204) { _dcSidecarCount--;
await dockerApi('DELETE', `/containers/${containerId}?force=true`).catch(() => {}); if (_dcSidecarCount <= 0) { _dcSidecarCount = 0; stopDeltacastBridge(); }
return jsonResponse(res, 502, { error: 'Failed to start container', details: startRes.data }); }
} return jsonResponse(res, 502, { error: 'Failed to create container', details: createRes.data });
}
jsonResponse(res, 201, { containerId, capturePort }); containerId = createRes.data.Id;
const _u = (env.find(e => e.startsWith('MAM_API_URL=')) || '').slice(12);
const _tok = env.some(e => e.startsWith('MAM_API_TOKEN=') && e.length > 14);
console.log(`[sidecar-start] ${containerId} image=${image} src=${sourceType} MAM_API_URL=${_u} token=${_tok}`);
const startRes = await dockerApi('POST', `/containers/${containerId}/start`);
if (startRes.status !== 204) {
if (sourceType === 'deltacast') {
_dcSidecarCount--;
if (_dcSidecarCount <= 0) { _dcSidecarCount = 0; stopDeltacastBridge(); }
}
await dockerApi('DELETE', `/containers/${containerId}?force=true`).catch(() => {});
return jsonResponse(res, 502, { error: 'Failed to start container', details: startRes.data });
}
if (sourceType === 'deltacast') _containerSourceType.set(containerId, 'deltacast');
jsonResponse(res, 201, { containerId, capturePort });
} catch (err) {
if (sourceType === 'deltacast') {
_dcSidecarCount--;
if (_dcSidecarCount <= 0) { _dcSidecarCount = 0; stopDeltacastBridge(); }
}
throw err;
}
} catch (err) { } catch (err) {
jsonResponse(res, 500, { error: err.message }); jsonResponse(res, 500, { error: err.message });
} }
@ -189,6 +365,19 @@ async function handleSidecarStop(containerId, res) {
console.log(`[sidecar-stop] ==== capture logs for ${containerId} ====\n${logs}\n[sidecar-stop] ==== end logs ====`); console.log(`[sidecar-stop] ==== capture logs for ${containerId} ====\n${logs}\n[sidecar-stop] ==== end logs ====`);
// Container has now exited gracefully (or hit the 180s cap); remove it. // Container has now exited gracefully (or hit the 180s cap); remove it.
await dockerApi('DELETE', `/containers/${containerId}?force=true`).catch(() => {}); await dockerApi('DELETE', `/containers/${containerId}?force=true`).catch(() => {});
// Deltacast bridge lifecycle: decrement sidecar count; stop bridge when last.
if (_containerSourceType.get(containerId) === 'deltacast') {
_containerSourceType.delete(containerId);
_dcSidecarCount--;
if (_dcSidecarCount <= 0) {
_dcSidecarCount = 0;
stopDeltacastBridge();
}
} else {
_containerSourceType.delete(containerId);
}
jsonResponse(res, 200, { ok: true }); jsonResponse(res, 200, { ok: true });
} catch (err) { } catch (err) {
console.error(`[sidecar-stop] error: ${err.message}`); console.error(`[sidecar-stop] error: ${err.message}`);
@ -227,6 +416,147 @@ async function handleSidecarStatus(containerId, res) {
} }
} }
// ── Agent auth ────────────────────────────────────────────────────────────
// When NODE_TOKEN is configured, privileged control endpoints (driver install)
// require a matching `Authorization: Bearer <NODE_TOKEN>`. mam-api forwards the
// node's stored token. If NODE_TOKEN is empty (dev), auth is not enforced.
function checkAgentAuth(req) {
if (!NODE_TOKEN) return true;
const hdr = req.headers['authorization'] || '';
const m = /^Bearer\s+(.+)$/i.exec(hdr);
return !!m && m[1] === NODE_TOKEN;
}
// ── Driver/SDK install ────────────────────────────────────────────────────
// Probe host presence of each capture-driver vendor. Mirrors the detection the
// install script uses, so the UI can show "installed / not installed" without
// running the installer. Best-effort: every probe is guarded.
function probeDriverStatus() {
const out = {};
// blackmagic — kernel module + /dev/blackmagic device tree.
let bmLoaded = false;
try { bmLoaded = fs.readFileSync('/proc/modules', 'utf8').split('\n').some(l => /^blackmagic\b/.test(l)); } catch (_) {}
let bmDev = false;
try { bmDev = fs.existsSync('/dev/blackmagic') && fs.readdirSync('/dev/blackmagic').length > 0; } catch (_) {}
out.blackmagic = { installed: bmLoaded || bmDev, module_loaded: bmLoaded, device_present: bmDev };
// aja — ajantv2 kernel module.
let ajaLoaded = false;
try { ajaLoaded = fs.readFileSync('/proc/modules', 'utf8').split('\n').some(l => /ajantv2/.test(l)); } catch (_) {}
out.aja = { installed: ajaLoaded, module_loaded: ajaLoaded };
// deltacast — videomaster module or /dev/deltacast* node.
let dcLoaded = false;
try { dcLoaded = fs.readFileSync('/proc/modules', 'utf8').split('\n').some(l => /videomaster/.test(l)); } catch (_) {}
let dcDev = false;
try { dcDev = fs.readdirSync('/dev').some(n => /^deltacast\d+$/.test(n)); } catch (_) {}
out.deltacast = { installed: dcLoaded || dcDev, module_loaded: dcLoaded, device_present: dcDev };
// ndi — user-space libs only. Look in the install target + common lib dirs.
let ndiPresent = false;
try {
for (const dir of ['/opt/ndi-lib', '/usr/local/lib', '/usr/lib/x86_64-linux-gnu']) {
let entries = [];
try { entries = fs.readdirSync(dir); } catch (_) { continue; }
if (entries.some(n => /^libndi\.so/.test(n))) { ndiPresent = true; break; }
}
} catch (_) {}
out.ndi = { installed: ndiPresent, libs_present: ndiPresent };
return out;
}
async function handleDriverStatus(res) {
try {
jsonResponse(res, 200, { kernel: os.release(), vendors: probeDriverStatus() });
} catch (err) {
jsonResponse(res, 500, { error: err.message });
}
}
// Run install-driver.sh <vendor> inside a one-shot PRIVILEGED ubuntu container.
// The repo is bind-mounted read-only at /repo; host kernel paths are mounted so
// dkms/modprobe/ldconfig affect the host. Logs are streamed back to the caller.
async function handleDriverInstall(body, res) {
const vendor = String(body?.vendor || '').toLowerCase();
if (!DRIVER_VENDORS.includes(vendor)) {
return jsonResponse(res, 400, { error: `Invalid vendor (allowed: ${DRIVER_VENDORS.join(', ')})` });
}
let containerId;
try {
// Host paths the installer needs to reach the host kernel:
// /lib/modules,/usr/src,/boot → DKMS / module build + install
// /dev → device-node visibility + udev
// The repo (sdk/<vendor>/ + deploy/install-driver.sh) is mounted read-only.
const binds = [
`${REPO_DIR}:/repo:ro`,
'/lib/modules:/lib/modules',
'/usr/src:/usr/src',
'/boot:/boot',
'/dev:/dev',
// NDI install target lives under /opt; expose host /opt so libs land on host.
'/opt:/opt',
];
const spec = {
Image: 'ubuntu:22.04',
// NOTE: vendor is a value from DRIVER_VENDORS only — never arbitrary input.
// Passed as a distinct argv element (Cmd array), not a shell string.
Cmd: ['bash', '/repo/deploy/install-driver.sh', vendor],
Env: [`REPO_DIR=/repo`],
WorkingDir: '/repo',
HostConfig: {
Privileged: true,
NetworkMode: 'host',
Binds: binds,
AutoRemove: false,
},
};
const createRes = await dockerApi('POST', '/containers/create', spec);
if (createRes.status !== 201) {
return jsonResponse(res, 502, { error: 'Failed to create install container', details: createRes.data });
}
containerId = createRes.data.Id;
console.log(`[driver-install] ${containerId} vendor=${vendor}`);
const startRes = await dockerApi('POST', `/containers/${containerId}/start`);
if (startRes.status !== 204) {
await dockerApi('DELETE', `/containers/${containerId}?force=true`).catch(() => {});
return jsonResponse(res, 502, { error: 'Failed to start install container', details: startRes.data });
}
// Wait for the install to finish (DKMS builds can take a minute+).
let exitCode = null;
for (let i = 0; i < 600; i++) {
await new Promise(r => setTimeout(r, 1000));
const inspect = await dockerApi('GET', `/containers/${containerId}/json`);
const state = inspect.data?.State;
if (state && !state.Running) { exitCode = state.ExitCode; break; }
}
const logs = await fetchContainerLogs(containerId);
const rebootRequired = /REBOOT_REQUIRED=1/.test(logs);
const ok = exitCode === 0;
jsonResponse(res, ok ? 200 : 500, {
ok,
vendor,
exitCode,
rebootRequired,
logs,
status: probeDriverStatus()[vendor] || null,
});
} catch (err) {
jsonResponse(res, 500, { error: err.message });
} finally {
if (containerId) {
await dockerApi('DELETE', `/containers/${containerId}?force=true`).catch(() => {});
}
}
}
// ── CPU sampling (500ms window) ─────────────────────────────────────────── // ── CPU sampling (500ms window) ───────────────────────────────────────────
function sampleCpu() { function sampleCpu() {
return new Promise(resolve => { return new Promise(resolve => {
@ -247,21 +577,39 @@ function sampleCpu() {
} }
// -- Live GPU utilization sampling ----------------------------------------- // -- Live GPU / NVENC encode telemetry sampling -----------------------------
// Spawns a short-lived nvidia container via Docker API on each heartbeat call. // Spawns a short-lived nvidia container via Docker API on each heartbeat call.
// Returns array of { index, util_pct, mem_used_mb, mem_total_mb } per GPU, // Returns array of { index, util_pct, enc_util_pct, mem_used_mb, mem_total_mb,
// or [] if no GPUs / nvidia runtime unavailable. // nvenc_sessions } per GPU, or [] if no GPUs / nvidia runtime unavailable.
//
// Two nvidia-smi queries are run inside one container via `sh -c`, each guarded
// with `|| true` so a query unsupported on a given driver/GPU (e.g. older cards
// that don't expose utilization.encoder) doesn't abort the whole sample:
// 1. --query-gpu → per-GPU gpu/encoder util + memory
// 2. --query-compute-apps → pid,used_memory,gpu_uuid for live processes; we
// count rows per GPU as an NVENC/compute "session" approximation. Marked
// with a SEP line so the two CSV blocks can be told apart in the log.
async function sampleGpuUtil() { async function sampleGpuUtil() {
if (!_gpuCache || _gpuCache.length === 0) return []; if (!_gpuCache || _gpuCache.length === 0) return [];
const QUERY = '--query-gpu=index,utilization.gpu,memory.used,memory.total'; const GPU_QUERY = '--query-gpu=index,utilization.gpu,utilization.encoder,memory.used,memory.total';
const FMT = '--format=csv,noheader,nounits'; const APP_QUERY = '--query-compute-apps=gpu_uuid,pid,used_memory';
const FMT = '--format=csv,noheader,nounits';
// Map GPU index → uuid so compute-app rows (keyed by uuid) attach to a GPU.
const UUID_QUERY = '--query-gpu=index,uuid';
const SCRIPT = [
`nvidia-smi ${GPU_QUERY} ${FMT} || true`,
`echo '---SEP-APPS---'`,
`nvidia-smi ${APP_QUERY} ${FMT} 2>/dev/null || true`,
`echo '---SEP-UUID---'`,
`nvidia-smi ${UUID_QUERY} ${FMT} 2>/dev/null || true`,
].join('; ');
let containerId; let containerId;
try { try {
const createRes = await dockerApi('POST', '/containers/create', { const createRes = await dockerApi('POST', '/containers/create', {
Image: 'ubuntu:22.04', Image: 'ubuntu:22.04',
Cmd: ['nvidia-smi', QUERY, FMT], Cmd: ['sh', '-c', SCRIPT],
HostConfig: { HostConfig: {
AutoRemove: false, AutoRemove: false,
Runtime: 'nvidia', Runtime: 'nvidia',
@ -295,11 +643,46 @@ async function sampleGpuUtil() {
}); });
const text = logRes.replace(/[\x00-\x07].{7}/g, '').trim(); const text = logRes.replace(/[\x00-\x07].{7}/g, '').trim();
const lines = text.split('\n').filter(l => /^\d+,/.test(l.trim())); const [gpuBlock = '', appBlock = '', uuidBlock = ''] =
text.split(/---SEP-(?:APPS|UUID)---/);
// uuid → index map (for attributing compute-app rows to a GPU)
const uuidToIndex = {};
uuidBlock.split('\n').forEach(l => {
const m = l.trim().match(/^(\d+)\s*,\s*(GPU-[0-9a-fA-F-]+)/);
if (m) uuidToIndex[m[2]] = parseInt(m[1], 10);
});
// NVENC/compute session count per GPU index (best-effort).
const sessionsByIndex = {};
appBlock.split('\n').forEach(l => {
const parts = l.split(',').map(s => s.trim());
const uuid = parts[0];
if (!uuid || !uuid.startsWith('GPU-')) return;
const idx = uuidToIndex[uuid];
if (idx == null) return;
sessionsByIndex[idx] = (sessionsByIndex[idx] || 0) + 1;
});
const lines = gpuBlock.split('\n').filter(l => /^\s*\d+\s*,/.test(l));
return lines.map(line => { return lines.map(line => {
const [idx, util, memUsed, memTotal] = line.split(',').map(s => parseInt(s.trim(), 10)); // utilization.encoder may report "[N/A]" on cards/drivers that don't
return { index: idx, util_pct: util, mem_used_mb: memUsed, mem_total_mb: memTotal }; // expose it — parseInt yields NaN there, which we coerce to null.
const cols = line.split(',').map(s => s.trim());
const idx = parseInt(cols[0], 10);
const util = parseInt(cols[1], 10);
const encUtil = parseInt(cols[2], 10);
const memUsed = parseInt(cols[3], 10);
const memTotal = parseInt(cols[4], 10);
return {
index: idx,
util_pct: Number.isNaN(util) ? null : util,
enc_util_pct: Number.isNaN(encUtil) ? null : encUtil,
mem_used_mb: Number.isNaN(memUsed) ? null : memUsed,
mem_total_mb: Number.isNaN(memTotal) ? null : memTotal,
nvenc_sessions: sessionsByIndex[idx] || 0,
};
}); });
} catch (err) { } catch (err) {
console.warn('[gpu-util] sampling failed:', err.message); console.warn('[gpu-util] sampling failed:', err.message);
@ -480,12 +863,31 @@ async function heartbeat() {
const ip_address = getIp(); const ip_address = getIp();
const capabilities = detectHardware(); const capabilities = detectHardware();
// Issue #166 — fold live NVENC/GPU encode telemetry into capabilities.gpus so
// the Cluster screen (which reads cluster_nodes.capabilities.gpus) can render
// per-GPU util / encoder util / NVENC sessions alongside the static name+VRAM.
// gpu_util is also sent verbatim below for any consumer reading metrics.gpus.
if (Array.isArray(capabilities.gpus) && gpu_util.length) {
capabilities.gpus = capabilities.gpus.map(g => {
const live = gpu_util.find(u => u.index === g.index);
if (!live) return g;
return {
...g,
util_pct: live.util_pct,
enc_util_pct: live.enc_util_pct,
mem_used_mb: live.mem_used_mb,
mem_total_mb: live.mem_total_mb ?? g.memory_mb ?? null,
nvenc_sessions: live.nvenc_sessions,
};
});
}
const payload = { const payload = {
hostname: os.hostname(), hostname: NODE_NAME,
ip_address, ip_address,
role: NODE_ROLE, role: NODE_ROLE,
version: VERSION, version: VERSION,
api_url: `http://${ip_address || os.hostname()}:${AGENT_PORT}`, api_url: `http://${ip_address || NODE_NAME}:${AGENT_PORT}`,
cpu_usage, cpu_usage,
mem_used_mb: Math.round((totalMem - freeMem) / 1048576), mem_used_mb: Math.round((totalMem - freeMem) / 1048576),
mem_total_mb: Math.round(totalMem / 1048576), mem_total_mb: Math.round(totalMem / 1048576),
@ -572,6 +974,16 @@ const server = http.createServer((req, res) => {
const id = pathname.slice('/sidecar/'.length, -'/status'.length); const id = pathname.slice('/sidecar/'.length, -'/status'.length);
handleSidecarStatus(id, res); handleSidecarStatus(id, res);
} else if (req.method === 'GET' && pathname === '/driver/status') {
if (!checkAgentAuth(req)) return jsonResponse(res, 401, { error: 'Unauthorized' });
handleDriverStatus(res);
} else if (req.method === 'POST' && pathname === '/driver/install') {
if (!checkAgentAuth(req)) return jsonResponse(res, 401, { error: 'Unauthorized' });
readBody(req)
.then(body => handleDriverInstall(body, res))
.catch(() => jsonResponse(res, 400, { error: 'Invalid request body' }));
} else if (req.method === 'GET' && pathname.startsWith('/live/')) { } else if (req.method === 'GET' && pathname.startsWith('/live/')) {
serveLiveFile(pathname, res); serveLiveFile(pathname, res);

View file

@ -78,10 +78,6 @@ RUN set -eux; \
echo "caspar binary: /opt/casparcg_server/bin/casparcg"; \ echo "caspar binary: /opt/casparcg_server/bin/casparcg"; \
cd /; rm -rf /tmp/caspar cd /; rm -rf /tmp/caspar
# ── NDI runtime (optional) ───────────────────────────────────────────────────
# If an NDI SDK tarball URL is provided, extract its libs to /opt/ndi-lib and
# point CasparCG at them via NDI_RUNTIME_DIR_V6. Pin the SDK version to what the
# server expects (the common docker failure is a libndi .so version mismatch).
RUN if [ -n "$NDI_SDK_URL" ]; then \ RUN if [ -n "$NDI_SDK_URL" ]; then \
mkdir -p /opt/ndi-lib && \ mkdir -p /opt/ndi-lib && \
curl -fsSL "$NDI_SDK_URL" -o /tmp/ndi.tar.gz && \ curl -fsSL "$NDI_SDK_URL" -o /tmp/ndi.tar.gz && \
@ -91,16 +87,13 @@ RUN if [ -n "$NDI_SDK_URL" ]; then \
fi fi
ENV NDI_RUNTIME_DIR_V6=/opt/ndi-lib ENV NDI_RUNTIME_DIR_V6=/opt/ndi-lib
# CasparCG media folder — mam-api stages assets from S3 into this volume.
RUN mkdir -p /media RUN mkdir -p /media
# ── Node control shim ────────────────────────────────────────────────────────
WORKDIR /app WORKDIR /app
COPY package*.json ./ COPY package*.json ./
RUN npm install --omit=dev RUN npm install --omit=dev
COPY . . COPY . .
# CasparCG config + entrypoint
COPY casparcg.config /opt/casparcg/casparcg.config COPY casparcg.config /opt/casparcg/casparcg.config
COPY entrypoint.sh /entrypoint.sh COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh RUN chmod +x /entrypoint.sh

View file

@ -1,8 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
# Headless GL: start a virtual framebuffer unless a real DISPLAY is provided
# (a GPU node may pass through an X socket). CasparCG's mixer needs a GL context.
if [ -z "${DISPLAY:-}" ]; then if [ -z "${DISPLAY:-}" ]; then
echo "[entrypoint] starting Xvfb on :99" echo "[entrypoint] starting Xvfb on :99"
Xvfb :99 -screen 0 1920x1080x24 -nolisten tcp & Xvfb :99 -screen 0 1920x1080x24 -nolisten tcp &

View file

@ -44,6 +44,20 @@ app.post('/transport/skip', async (req, res) => { try { res.json(await playout
app.post('/transport/pause', async (req, res) => { try { res.json(await playoutManager.pause()); } catch (e) { res.status(500).json({ error: e.message }); } }); app.post('/transport/pause', async (req, res) => { try { res.json(await playoutManager.pause()); } catch (e) { res.status(500).json({ error: e.message }); } });
app.post('/transport/resume', async (req, res) => { try { res.json(await playoutManager.resume()); } catch (e) { res.status(500).json({ error: e.message }); } }); app.post('/transport/resume', async (req, res) => { try { res.json(await playoutManager.resume()); } catch (e) { res.status(500).json({ error: e.message }); } });
// Fire an SCTE-35 ad-break splice on the live output. Body:
// { eventId, type: 'splice_insert'|'immediate'|'splice_out'|'splice_in', durationS }
// Returns the active-break descriptor (or the splice_in ack) so the mam-api can
// stamp the as-run log.
app.post('/scte/trigger', (req, res) => {
try {
const { eventId = 1, type = 'splice_insert', durationS = 30 } = req.body || {};
res.json(playoutManager.triggerScte({ eventId, type, durationS }));
} catch (err) {
console.error('[playout] /scte/trigger error:', err.message);
res.status(500).json({ error: err.message });
}
});
app.get('/status', (req, res) => res.json(playoutManager.getStatus())); app.get('/status', (req, res) => res.json(playoutManager.getStatus()));
// Auto-start: when the sidecar is spawned by mam-api with channel env, bring up // Auto-start: when the sidecar is spawned by mam-api with channel env, bring up

View file

@ -1,6 +1,6 @@
import { AmcpClient } from './amcp.js'; import { AmcpClient } from './amcp.js';
import { spawn } from 'node:child_process'; import { spawn } from 'node:child_process';
import { mkdirSync } from 'node:fs'; import { mkdirSync, readdirSync, unlinkSync } from 'node:fs';
// Playout manager — owns one CasparCG channel's lifecycle inside this sidecar. // Playout manager — owns one CasparCG channel's lifecycle inside this sidecar.
// //
@ -83,8 +83,13 @@ export class PlayoutManager {
currentClip: null, currentClip: null,
startedAt: null, startedAt: null,
lastError: null, lastError: null,
// SCTE-35: the currently-active ad break, if any. Set by triggerScte and
// cleared by a timer when the break window elapses. Surfaced in getStatus
// so the UI can render an "in break" state + countdown.
scteActive: null, // { eventId, type, durationS, firedAt(iso), endsAt(iso) }
}; };
this._advanceTimer = null; this._advanceTimer = null;
this._scteTimer = null;
this._hlsProc = null; // standalone ffmpeg re-mux child process this._hlsProc = null; // standalone ffmpeg re-mux child process
this._hlsRestartTimer = null; this._hlsRestartTimer = null;
} }
@ -211,6 +216,20 @@ export class PlayoutManager {
_startHlsRemux() { _startHlsRemux() {
if (!HLS_DIR) return; if (!HLS_DIR) return;
try { mkdirSync(HLS_DIR, { recursive: true }); } catch (_) {} try { mkdirSync(HLS_DIR, { recursive: true }); } catch (_) {}
// Purge stale HLS artifacts from any prior session before starting. The
// /media volume is a shared host bind, so a previous (or duplicate/failover)
// sidecar can leave orphaned index*.ts + an old index.m3u8 behind. ffmpeg's
// index%d.ts counter restarts at 0, so those leftovers collide with the new
// segment numbering and can briefly corrupt the live playlist hls.js reads
// (it sees a frozen / non-monotonic edge → monitor goes black). A clean dir
// per session guarantees a coherent live timeline.
try {
for (const f of readdirSync(HLS_DIR)) {
if (/\.ts$/.test(f) || /\.m3u8$/.test(f)) {
try { unlinkSync(`${HLS_DIR}/${f}`); } catch (_) {}
}
}
} catch (_) {}
this._stopHlsRemux(); this._stopHlsRemux();
const out = `${HLS_DIR}/index.m3u8`; const out = `${HLS_DIR}/index.m3u8`;
@ -291,6 +310,7 @@ export class PlayoutManager {
async stopChannel() { async stopChannel() {
this._clearAdvance(); this._clearAdvance();
this._clearScte();
this.state.running = false; // set first so the ffmpeg exit handler won't respawn this.state.running = false; // set first so the ffmpeg exit handler won't respawn
this._stopHlsRemux(); this._stopHlsRemux();
try { await this.amcp.send(`STOP ${CHANNEL}-${FG_LAYER}`); } catch (_) {} try { await this.amcp.send(`STOP ${CHANNEL}-${FG_LAYER}`); } catch (_) {}
@ -416,6 +436,92 @@ export class PlayoutManager {
return this.getStatus(); return this.getStatus();
} }
// ── SCTE-35 ad-break splice ──────────────────────────────────────────────
// Act on an ad-break cue. The mam-api owns scheduling + persistence; the
// sidecar performs the actual splice on the live output and tracks the active
// break locally so /status can report a countdown.
//
// What this does today, end to end:
// 1. Records the break as the active break (UI reads it from /status for the
// "SCTE BREAK" on-air state + countdown). A timer clears it after
// durationS so the UI returns to normal automatically.
// 2. Emits an operator-visible log line at the splice point.
// 3. Returns the cue descriptor so the mam-api can stamp the as-run log.
//
// ── Real in-stream SCTE-35 injection (the injection point) ─────────────────
// True SCTE-35 requires inserting a splice_info_section into the OUTPUT
// transport stream on a dedicated SCTE-35 PID, time-aligned to the splice
// point (pts_time). CasparCG 2.3's FFMPEG consumer does NOT expose an SCTE-35
// muxer option, so we cannot ask CasparCG to carry the cue. The two viable
// production paths, neither of which the current single-process CasparCG
// output supports out of the box, are:
//
// (a) ffmpeg-based output: when the primary consumer is replaced by a
// Node-spawned ffmpeg (as the HLS preview re-mux already is), mux an
// SCTE-35 data stream. ffmpeg can pass through a -map'd scte35 PID, and
// for HLS can emit #EXT-X-CUE-OUT/#EXT-X-CUE-IN (or DATERANGE) tags. The
// hook would build the splice_insert binary section here and feed it to
// that ffmpeg via a data input / sidecar packetizer.
// (b) A downstream SCTE-35 inserter (e.g. an OTT packager / encoder that
// accepts cue triggers over its own API). The hook would POST the cue
// to that device's API at the splice instant.
//
// Until one of those output paths is wired, the splice is faithfully
// scheduled, triggered, countdown-tracked, and as-run-logged — but the cue is
// NOT yet embedded in the SRT/RTMP/SDI/NDI elementary stream. Replace the body
// of _injectScteCue below to enable real injection.
triggerScte({ eventId = 1, type = 'splice_insert', durationS = 30 } = {}) {
const firedAt = new Date();
const endsAt = new Date(firedAt.getTime() + (durationS > 0 ? durationS * 1000 : 0));
// Build + emit the cue on the output (TODO injection point — see above).
this._injectScteCue({ eventId, type, durationS });
// A splice_in / return-to-network ends any active break immediately.
if (type === 'splice_in') {
this._clearScte();
console.log(`[playout][scte] splice_in event=${eventId} — return to network`);
return { eventId, type, durationS: 0, firedAt: firedAt.toISOString(), endsAt: firedAt.toISOString() };
}
this.state.scteActive = {
eventId, type, durationS,
firedAt: firedAt.toISOString(),
endsAt: endsAt.toISOString(),
};
console.log(`[playout][scte] ${type} event=${eventId} duration=${durationS}s — splice OUT at ${firedAt.toISOString()}`);
// Auto-clear the active break when its window elapses (splice_out is
// open-ended, so it stays until an explicit splice_in).
if (this._scteTimer) { clearTimeout(this._scteTimer); this._scteTimer = null; }
if (durationS > 0 && type !== 'splice_out') {
this._scteTimer = setTimeout(() => {
this._scteTimer = null;
console.log(`[playout][scte] break event=${eventId} ended — return to network`);
this._clearScte();
}, durationS * 1000);
}
return this.state.scteActive;
}
// The SCTE-35 cue packetizer / injection hook. See the long comment on
// triggerScte for why this is a stub on the current CasparCG output path and
// what to put here to enable real in-stream injection.
_injectScteCue({ eventId, type, durationS }) {
// TODO(scte-injection): build the splice_info_section (splice_insert with
// splice_event_id=eventId, out_of_network_indicator per type,
// break_duration=durationS*90000 ticks) and emit it on the output's SCTE-35
// PID via an ffmpeg-based output, or POST it to a downstream inserter's API.
// No-op until the output path supports it; the scheduling/trigger/as-run
// path above is fully functional regardless.
return null;
}
_clearScte() {
if (this._scteTimer) { clearTimeout(this._scteTimer); this._scteTimer = null; }
this.state.scteActive = null;
}
_reportAsRunStart(item) { _reportAsRunStart(item) {
// The mam-api owns the as-run table; the sidecar just logs locally. The API // The mam-api owns the as-run table; the sidecar just logs locally. The API
// polls /status and writes as-run rows on clip change. Keeping the DB write // polls /status and writes as-run rows on clip change. Keeping the DB write
@ -437,6 +543,7 @@ export class PlayoutManager {
loop: this.state.loop, loop: this.state.loop,
startedAt: this.state.startedAt, startedAt: this.state.startedAt,
lastError: this.state.lastError, lastError: this.state.lastError,
scteActive: this.state.scteActive || null,
}; };
} }
} }

View file

@ -23,6 +23,18 @@ function App() {
try { localStorage.setItem('df.sidebar.collapsed', next ? '1' : '0'); } catch {} try { localStorage.setItem('df.sidebar.collapsed', next ? '1' : '0'); } catch {}
return next; return next;
}); });
// Sync route state with URL hash
React.useEffect(() => {
const parseHash = () => {
const hash = window.location.hash.slice(1); // remove #
const route = hash.startsWith('/') ? hash.slice(1) : hash || 'home';
setRoute(route);
};
parseHash();
window.addEventListener('hashchange', parseHash);
return () => window.removeEventListener('hashchange', parseHash);
}, []);
}, []); }, []);
React.useEffect(() => { React.useEffect(() => {

View file

@ -28,16 +28,29 @@ window.ZAMPP_API_PREFIX = API; // single source of truth (#115)
// The panel is now a UXP plugin (.ccx), replacing the legacy CEP/ZXP panel. // The panel is now a UXP plugin (.ccx), replacing the legacy CEP/ZXP panel.
// Each entry carries a `ccx` URL; the older `zxp`/`installer` fields are gone. // Each entry carries a `ccx` URL; the older `zxp`/`installer` fields are gone.
window.PREMIERE_RELEASES = [ window.PREMIERE_RELEASES = [
{
version: '2.2.3',
ccx: '/downloads/dragonflight-mam-2.2.3.ccx',
installer: null,
notes: 'Fix: streaming write for large imports (no more truncated files). UXP plugin: one-click Export Timeline, library + conform + auto-relink, growing-file mount.',
latest: true,
},
{ {
version: '2.2.2', version: '2.2.2',
ccx: '/downloads/dragonflight-mam-2.2.2.ccx', ccx: '/downloads/dragonflight-mam-2.2.2.ccx',
installer: null, installer: null,
notes: 'UXP plugin: one-click Export Timeline, library + conform + auto-relink, growing-file mount, runtime version chip. Replaces the legacy CEP/ZXP panel.', notes: 'UXP plugin: one-click Export Timeline, library + conform + auto-relink, growing-file mount, runtime version chip. Replaces the legacy CEP/ZXP panel.',
latest: true, latest: false,
}, },
]; ];
window.PREMIERE_LATEST = window.PREMIERE_RELEASES.find(r => r.latest) || window.PREMIERE_RELEASES[0]; window.PREMIERE_LATEST = window.PREMIERE_RELEASES.find(r => r.latest) || window.PREMIERE_RELEASES[0];
// Teams ISO workstation installer. Placeholder slot: the .exe is not in the
// repo yet, so `available` is false and the Downloads modal renders the row
// disabled with a "coming soon" note. Drop the file into public/downloads/
// and flip `available: true` (set `version`) to finish it.
window.TEAMS_ISO = { version: null, url: '/downloads/TeamsISO.exe', available: false };
window.ZAMPP_DATA = { window.ZAMPP_DATA = {
PROJECTS: [], PROJECTS: [],
ASSETS: [], ASSETS: [],

View file

@ -153,7 +153,7 @@ function NewRecorderModal({ open, onClose }) {
const [recCodec, setRecCodec] = React.useState('hevc_nvenc'); const [recCodec, setRecCodec] = React.useState('hevc_nvenc');
// Custom target bitrate in Mbps for bitrate-controlled codecs (NVENC / x264 / // Custom target bitrate in Mbps for bitrate-controlled codecs (NVENC / x264 /
// x265 / DNxHD). ProRes ignores bitrate (quality is profile-driven). // x265 / DNxHD). ProRes ignores bitrate (quality is profile-driven).
const [recBitrate, setRecBitrate] = React.useState('60'); const [recBitrate, setRecBitrate] = React.useState('25');
// Container is derived from the codec, not manually chosen: HEVC/ProRes/DNxHR // Container is derived from the codec, not manually chosen: HEVC/ProRes/DNxHR
// MOV (fragmented, growing-capable); H.264 MP4. // MOV (fragmented, growing-capable); H.264 MP4.
const recContainer = (recCodec === 'h264' || recCodec === 'h264_nvenc' || recCodec === 'libx264') ? 'mp4' : 'mov'; const recContainer = (recCodec === 'h264' || recCodec === 'h264_nvenc' || recCodec === 'libx264') ? 'mp4' : 'mov';
@ -162,6 +162,12 @@ function NewRecorderModal({ open, onClose }) {
const codecUsesBitrate = BITRATE_CODECS.has(recCodec); const codecUsesBitrate = BITRATE_CODECS.has(recCodec);
const [proxyOn, setProxyOn] = React.useState(true); const [proxyOn, setProxyOn] = React.useState(true);
const [growingOn, setGrowingOn] = React.useState(false); const [growingOn, setGrowingOn] = React.useState(false);
// Growing-files mode forces the master to XDCAM HD422 / MXF OP1a in the capture
// backend (the only growing format Premiere can import live), but the target
// bitrate is still operator-controlled and applied via -b:v. Keep the bitrate
// input visible/editable whenever growing is on, even if the selected (and
// soon-to-be-overridden) codec would normally be quality-driven (ProRes).
const showBitrate = codecUsesBitrate || growingOn;
const [projectId, setProjectId] = React.useState(PROJECTS[0]?.id || ''); const [projectId, setProjectId] = React.useState(PROJECTS[0]?.id || '');
const [submitting, setSubmitting] = React.useState(false); const [submitting, setSubmitting] = React.useState(false);
const [submitErr, setSubmitErr] = React.useState(null); const [submitErr, setSubmitErr] = React.useState(null);
@ -214,8 +220,10 @@ function NewRecorderModal({ open, onClose }) {
recording_framerate: '', // empty = match source recording_framerate: '', // empty = match source
recording_resolution: 'native', recording_resolution: 'native',
}; };
// Custom bitrate only applies to bitrate-controlled codecs (ProRes ignores it). // Custom bitrate applies to bitrate-controlled codecs AND to growing-files
if (codecUsesBitrate && recBitrate) { // mode (which forces H.264/TS in capture but still honors -b:v). ProRes
// without growing ignores bitrate, so we omit it there.
if ((codecUsesBitrate || growingOn) && recBitrate) {
body.recording_video_bitrate = `${recBitrate}M`; body.recording_video_bitrate = `${recBitrate}M`;
} }
@ -224,7 +232,11 @@ function NewRecorderModal({ open, onClose }) {
} else if (sourceType === 'RTMP') { } else if (sourceType === 'RTMP') {
body.source_config = { url: rtmpUrl }; body.source_config = { url: rtmpUrl };
} else if (sourceType === 'DELTACAST') { } else if (sourceType === 'DELTACAST') {
body.source_config = {}; // One Deltacast board (index 0) exposes 8 channels. The picker's selected
// index IS the capture channel, so persist it as source_config.port; the
// capture sidecar maps that to the bridge's --port. device_index is kept
// for backward-compatible display/fallback.
body.source_config = { port: dcDeviceIdx };
body.device_index = dcDeviceIdx; body.device_index = dcDeviceIdx;
body.node_id = dcNodeId || undefined; body.node_id = dcNodeId || undefined;
} else { } else {
@ -397,10 +409,34 @@ function NewRecorderModal({ open, onClose }) {
</div> </div>
<div className="modal-section-body"> <div className="modal-section-body">
{recTab === 'video' && ( {recTab === 'video' && (
<>
{/* Codec presets one click fills codec + bitrate with a known-good
combo that passes the server-side validateRecorderConfig guard.
Container is derived from the codec (HEVC/ProRes/DNxHR MOV,
H.264 MP4), and master audio is always PCM (valid in MOV). */}
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginBottom: 12 }}>
{[
{ id: 'hevc', label: 'HEVC Master (MOV)', codec: 'hevc_nvenc', bitrate: '25' },
{ id: 'h264', label: 'H.264 Proxy-friendly (MP4)', codec: 'h264_nvenc', bitrate: '25' },
{ id: 'dnxhr', label: 'DNxHR HQ (MOV)', codec: 'dnxhr_hq', bitrate: '145' },
].map(p => (
<button key={p.id}
className={`btn ghost sm${recCodec === p.codec ? ' active' : ''}`}
onClick={() => { setRecCodec(p.codec); setRecBitrate(p.bitrate); }}
style={{ flexShrink: 0 }}>
{p.label}
</button>
))}
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}> <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
<div className="field"> <div className="field">
<label className="field-label">Video codec</label> <label className="field-label">
<select className="field-input" value={recCodec} onChange={e => setRecCodec(e.target.value)} style={{ appearance: 'auto' }}> Video codec{growingOn && <span style={{ marginLeft: 6, fontSize: 10, fontWeight: 700, letterSpacing: '0.06em', color: 'var(--warn, #d9a441)' }}>· LOCKED BY GROWING</span>}
</label>
<select className="field-input" value={growingOn ? 'h264_growing' : recCodec}
onChange={e => setRecCodec(e.target.value)} disabled={growingOn}
style={{ appearance: 'auto', opacity: growingOn ? 0.6 : 1 }}>
{growingOn && <option value="h264_growing">XDCAM HD422 (MXF OP1a) growing</option>}
<option value="hevc_nvenc">All-Intra HEVC (NVENC) GPU, growing</option> <option value="hevc_nvenc">All-Intra HEVC (NVENC) GPU, growing</option>
<option value="h264_nvenc">H.264 (NVENC) GPU</option> <option value="h264_nvenc">H.264 (NVENC) GPU</option>
<option value="prores_hq">ProRes 422 HQ 4:2:2 CPU</option> <option value="prores_hq">ProRes 422 HQ 4:2:2 CPU</option>
@ -412,7 +448,7 @@ function NewRecorderModal({ open, onClose }) {
<option value="libx265">H.265 (x265, CPU)</option> <option value="libx265">H.265 (x265, CPU)</option>
</select> </select>
</div> </div>
{codecUsesBitrate ? ( {showBitrate ? (
<div className="field"> <div className="field">
<label className="field-label">Target bitrate (Mbps)</label> <label className="field-label">Target bitrate (Mbps)</label>
<input <input
@ -444,6 +480,7 @@ function NewRecorderModal({ open, onClose }) {
return null; return null;
})()} })()}
</div> </div>
</>
)} )}
{recTab === 'audio' && ( {recTab === 'audio' && (
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}> <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
@ -455,8 +492,10 @@ function NewRecorderModal({ open, onClose }) {
)} )}
{recTab === 'container' && ( {recTab === 'container' && (
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}> <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
<Field label="Container" value={recContainer === 'mp4' ? 'MP4 (auto)' : 'Fragmented MOV (auto)'} select /> <Field label="Container"
<Field label="Growing-file" value={recContainer === 'mov' ? 'Supported (edit-while-record)' : 'No'} select /> value={growingOn ? 'MXF OP1a (growing, auto)' : (recContainer === 'mp4' ? 'MP4 (auto)' : 'Fragmented MOV (auto)')} select />
<Field label="Growing-file"
value={growingOn ? 'On (edit-while-record, locked)' : (recContainer === 'mov' ? 'Supported (edit-while-record)' : 'No')} select />
</div> </div>
)} )}
</div> </div>
@ -486,6 +525,14 @@ function NewRecorderModal({ open, onClose }) {
Write the live master to the SMB share so editors can cut while it's still recording. Write the live master to the SMB share so editors can cut while it's still recording.
Requires the SMB share to be configured in Settings Storage. Requires the SMB share to be configured in Settings Storage.
</div> </div>
{growingOn && (
<div style={{ fontSize: 11, color: 'var(--warn, #d9a441)', marginTop: 6 }}>
Growing-files mode records XDCAM HD422 (MPEG-2 4:2:2 CBR) in MXF OP1a the format Premiere supports for edit-while-record growing files. Bitrate below still applies.
Premiere can import while it's still being written. The codec and container above
are overridden for this recorder (the target bitrate still applies). Turn growing
off to record your selected master codec/container.
</div>
)}
</div> </div>
</div> </div>

View file

@ -11,6 +11,12 @@ function _normalizeNode(n, x, y) {
index: g.index ?? 0, index: g.index ?? 0,
device: g.device || null, device: g.device || null,
bound: !!(g.name && g.memory_mb), // name+memory = nvidia-smi confirmed driver bound bound: !!(g.name && g.memory_mb), // name+memory = nvidia-smi confirmed driver bound
// Issue #166 live NVENC/GPU encode telemetry folded into capabilities.gpus
// by the node-agent heartbeat (null until a heartbeat carries it / a GPU node).
utilPct: g.util_pct != null ? g.util_pct : null,
encUtilPct: g.enc_util_pct != null ? g.enc_util_pct : null,
memUsedMb: g.mem_used_mb != null ? g.mem_used_mb : null,
nvencSessions: g.nvenc_sessions != null ? g.nvenc_sessions : null,
})); }));
// Blackmagic DeckLink: capabilities.blackmagic + capabilities.blackmagic_model // Blackmagic DeckLink: capabilities.blackmagic + capabilities.blackmagic_model
@ -21,6 +27,13 @@ function _normalizeNode(n, x, y) {
online: b.online !== false, online: b.online !== false,
})); }));
// Deltacast ports used by the Capture Drivers panel as a secondary
// "driver present?" signal (heartbeat only reports a port if the card is seen).
const deltacastPorts = (cap.deltacast || []).map(d => ({
index: d.index ?? 0,
device: d.device || null,
}));
const memUsedMb = n.mem_used_mb || n.memory_used_mb || (n.mem && n.mem < 1000 ? n.mem * 1024 : n.mem || 0); const memUsedMb = n.mem_used_mb || n.memory_used_mb || (n.mem && n.mem < 1000 ? n.mem * 1024 : n.mem || 0);
const memTotalMb = n.mem_total_mb || n.memory_total_mb || (n.memTotal && n.memTotal < 1000 ? n.memTotal * 1024 : n.memTotal || 0); const memTotalMb = n.mem_total_mb || n.memory_total_mb || (n.memTotal && n.memTotal < 1000 ? n.memTotal * 1024 : n.memTotal || 0);
@ -38,6 +51,7 @@ function _normalizeNode(n, x, y) {
// Raw capabilities for the hardware panel // Raw capabilities for the hardware panel
gpus, gpus,
bmdPorts, bmdPorts,
deltacastPorts,
// Legacy flat arrays kept for the stat-row summary cards // Legacy flat arrays kept for the stat-row summary cards
gpuCount: gpus.length, gpuCount: gpus.length,
bmdCount: bmdPorts.length, bmdCount: bmdPorts.length,
@ -116,6 +130,7 @@ function Users() {
const [editingUser, setEditingUser] = React.useState(null); const [editingUser, setEditingUser] = React.useState(null);
const [resetUser, setResetUser] = React.useState(null); const [resetUser, setResetUser] = React.useState(null);
const [menuFor, setMenuFor] = React.useState(null); // row id whose menu is open const [menuFor, setMenuFor] = React.useState(null); // row id whose menu is open
const [confirm, confirmModal] = window.useConfirm();
const refreshUsers = React.useCallback(() => { const refreshUsers = React.useCallback(() => {
window.ZAMPP_API.fetch('/users') window.ZAMPP_API.fetch('/users')
@ -161,9 +176,9 @@ function Users() {
const onCreated = () => { refreshUsers(); setShowInvite(false); }; const onCreated = () => { refreshUsers(); setShowInvite(false); };
const deleteUser = (u) => { const deleteUser = async (u) => {
setMenuFor(null); setMenuFor(null);
if (!confirm(`Delete user "${u.name}" (@${u.username})?\nThis cannot be undone.`)) return; if (!(await confirm({ title: 'Delete user?', message: `Delete user "${u.name}" (@${u.username})?\nThis cannot be undone.` }))) return;
window.ZAMPP_API.fetch('/users/' + u.id, { method: 'DELETE' }) window.ZAMPP_API.fetch('/users/' + u.id, { method: 'DELETE' })
.then(refreshUsers) .then(refreshUsers)
.catch(e => alert('Delete failed: ' + e.message)); .catch(e => alert('Delete failed: ' + e.message));
@ -180,6 +195,7 @@ function Users() {
return ( return (
<div className="page"> <div className="page">
{confirmModal}
<div className="page-header"> <div className="page-header">
<h1>Users &amp; Groups</h1> <h1>Users &amp; Groups</h1>
<div className="spacer" /> <div className="spacer" />
@ -258,28 +274,7 @@ function Users() {
{tab === 'groups' && <GroupsPanel groups={groups} users={users} onChange={refreshGroups} />} {tab === 'groups' && <GroupsPanel groups={groups} users={users} onChange={refreshGroups} />}
{tab === 'policies' && ( {tab === 'policies' && (
<div className="panel" style={{ padding: '32px 24px', color: 'var(--text-2)' }}> <PoliciesPanel users={users} onChange={refreshUsers} />
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
<Icon name="lock" size={16} />
<div style={{ fontWeight: 600, fontSize: 14 }}>Access model</div>
</div>
<div style={{ fontSize: 12.5, color: 'var(--text-3)', lineHeight: 1.7, maxWidth: 640 }}>
<div style={{ marginBottom: 8 }}>
<strong style={{ color: 'var(--text-2)' }}>admin</strong> full access to every
project plus user, group, cluster, and system administration.
</div>
<div style={{ marginBottom: 8 }}>
<strong style={{ color: 'var(--text-2)' }}>editor / viewer</strong> see only the
projects they've been granted. A <em>view</em> grant is read-only; an
<em> edit</em> grant allows changes. Grants can target an individual user or a group.
</div>
<div>
Manage a project's grants from the <strong style={{ color: 'var(--text-2)' }}>Projects</strong> page
a project's <em>Manage access</em> menu. Group membership is managed on the
Groups tab above.
</div>
</div>
</div>
)} )}
</div> </div>
{showInvite && <InviteUserModal onCreated={onCreated} onClose={() => setShowInvite(false)} />} {showInvite && <InviteUserModal onCreated={onCreated} onClose={() => setShowInvite(false)} />}
@ -299,6 +294,206 @@ function Users() {
); );
} }
//
// PoliciesPanel - interactive per-user permission matrix for the Policies tab.
// Keeps the access-model explainer as a small header, then renders one row per
// user with: inline role <select> (PATCH /users/:id), a 2FA badge driven by
// totp_enabled, an admin-only "Reset 2FA" action (POST /users/:id/totp/disable,
// 204), and an Access expander backed by GET /users/:id/access.
//
function PoliciesPanel({ users, onChange }) {
const [expandedId, setExpandedId] = React.useState(null);
const [err, setErr] = React.useState(null);
const [confirm, confirmModal] = window.useConfirm();
const changeRole = (u, newRole) => {
if (u.role === newRole) return;
setErr(null);
window.ZAMPP_API.fetch('/users/' + u.id, { method: 'PATCH', body: JSON.stringify({ role: newRole }) })
.then(() => onChange && onChange())
.catch(e => setErr('Role change failed: ' + (e.message || e)));
};
// Reset 2FA uses a raw fetch because ZAMPP_API.fetch throws on the 204 (no JSON
// body). Mirrors the disable() pattern in TotpSection.
const resetTotp = async (u) => {
if (!(await confirm({ title: 'Reset two-factor?', message: `Reset two-factor for "${u.name}" (@${u.username})?\nThey will be able to sign in without a code until they re-enrol.`, confirmLabel: 'Reset 2FA' }))) return;
setErr(null);
fetch('/api/v1/users/' + u.id + '/totp/disable', {
method: 'POST',
credentials: 'include',
headers: { 'X-Requested-With': 'dragonflight-ui' },
})
.then(r => {
if (r.status === 204) { onChange && onChange(); return; }
return r.json().catch(() => ({})).then(b => { throw new Error(b.error || ('Failed (' + r.status + ')')); });
})
.catch(e => setErr('Reset 2FA failed: ' + (e.message || e)));
};
return (
<div>
{confirmModal}
{/* Access-model explainer (kept from the old static tab, condensed) */}
<div className="panel" style={{ padding: '16px 20px', marginBottom: 12, color: 'var(--text-2)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
<Icon name="lock" size={15} />
<div style={{ fontWeight: 600, fontSize: 13.5 }}>Access model</div>
</div>
<div style={{ fontSize: 12, color: 'var(--text-3)', lineHeight: 1.6, maxWidth: 720 }}>
<strong style={{ color: 'var(--text-2)' }}>admin</strong> has full access to every project plus
user, group, cluster, and system administration. <strong style={{ color: 'var(--text-2)' }}>editor / viewer</strong> see
only the projects they're granted a <em>view</em> grant is read-only, an <em>edit</em> grant
allows changes, and grants can target a user or a group. Edit per-project grants from the{' '}
<a href="#" onClick={e => { e.preventDefault(); window.dispatchEvent(new CustomEvent('df:nav', { detail: 'projects' })); }}
style={{ color: 'var(--accent-text)' }}>Projects</a> page; manage group membership on the Groups tab above.
</div>
</div>
{err && <div style={{ fontSize: 12, color: 'var(--danger)', marginBottom: 8 }}>{err}</div>}
<div className="panel">
<div className="user-row head">
<div>User</div>
<div>Role</div>
<div>2FA</div>
<div>Access</div>
<div></div>
</div>
{users.length === 0 && (
<div style={{ padding: '32px 0', textAlign: 'center', color: 'var(--text-3)' }}>No users found</div>
)}
{users.map(u => (
<UserPolicyRow key={u.id} user={u}
expanded={expandedId === u.id}
onToggle={() => setExpandedId(expandedId === u.id ? null : u.id)}
onChangeRole={changeRole}
onResetTotp={resetTotp} />
))}
</div>
</div>
);
}
function UserPolicyRow({ user: u, expanded, onToggle, onChangeRole, onResetTotp }) {
const [access, setAccess] = React.useState(null); // null = not loaded, {} once fetched
const [loading, setLoading] = React.useState(false);
const [accessErr, setAccessErr] = React.useState(null);
// Lazily fetch GET /users/:id/access the first time the row is expanded.
React.useEffect(() => {
if (!expanded || access !== null) return;
setLoading(true); setAccessErr(null);
window.ZAMPP_API.fetch('/users/' + u.id + '/access')
.then(d => setAccess(d || {}))
.catch(e => { setAccess({}); setAccessErr(e.message || 'Failed to load access'); })
.finally(() => setLoading(false));
}, [expanded, access, u.id]);
const projects = (access && access.projects) || [];
const memberships = (access && (access.groups || access.memberships)) || [];
return (
<div style={{ borderBottom: '1px solid var(--border)' }}>
<div className="user-row" style={{ borderBottom: 'none' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<div className="avatar" style={{ width: 32, height: 32, fontSize: 11, background: avatarColor(u.initials || u.id) }}>{u.initials || '??'}</div>
<div>
<div style={{ fontWeight: 500, fontSize: 13 }}>{u.name}</div>
<div className="mono" style={{ fontSize: 11, color: 'var(--text-3)' }}>@{u.username}</div>
</div>
</div>
<div>
<select value={u.role || 'viewer'}
onChange={e => onChangeRole(u, e.target.value)}
className="field-input"
style={{ width: 90, padding: '3px 6px', fontSize: 11.5, appearance: 'auto' }}>
<option value="admin">admin</option>
<option value="editor">editor</option>
<option value="viewer">viewer</option>
</select>
</div>
<div>
{u.totp_enabled
? <span className="badge success"><Icon name="key" size={10} /> 2FA on</span>
: <span className="badge neutral">2FA off</span>}
</div>
<div>
<button className="btn ghost sm" onClick={onToggle}>
{expanded ? 'Hide' : 'View'}
</button>
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
{u.totp_enabled && (
<button className="btn ghost sm danger" onClick={() => onResetTotp(u)} title="Disable this user's two-factor">
<Icon name="key" size={11} />Reset 2FA
</button>
)}
</div>
</div>
{expanded && (
<div style={{ padding: '0 16px 16px 16px', background: 'var(--bg-2)' }}>
{loading && <div style={{ padding: '12px 0', fontSize: 12.5, color: 'var(--text-3)' }}>Loading access</div>}
{accessErr && <div style={{ padding: '8px 0', fontSize: 12, color: 'var(--danger)' }}>{accessErr}</div>}
{!loading && !accessErr && (u.role === 'admin') && (
<div style={{ padding: '12px 0', fontSize: 12.5, color: 'var(--text-3)', display: 'flex', alignItems: 'center', gap: 6 }}>
<Icon name="check" size={12} style={{ color: 'var(--success)' }} />
Admin full access to every project.
</div>
)}
{!loading && !accessErr && u.role !== 'admin' && (
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, paddingTop: 12 }}>
{/* Accessible projects */}
<div>
<div style={{ fontSize: 11, color: 'var(--text-3)', textTransform: 'uppercase', letterSpacing: '0.06em', fontWeight: 600, marginBottom: 8 }}>
Projects ({projects.length})
</div>
{projects.length === 0 && (
<div style={{ fontSize: 12, color: 'var(--text-4)' }}>No project access granted.</div>
)}
{projects.map(p => {
// Backend `via` is 'direct' for a user grant, or 'group:<name>'
// when inherited from a group. Split the label off the prefix.
const via = p.via || 'direct';
const isGroup = via.indexOf('group') === 0;
const viaLabel = isGroup ? (via.indexOf(':') >= 0 ? via.slice(via.indexOf(':') + 1) : 'group') : 'direct';
return (
<div key={(p.project_id || p.id) + ':' + via}
style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '6px 0', borderBottom: '1px solid var(--border)' }}>
<span style={{ fontSize: 12.5, flex: 1 }}>{p.project_name || p.name || p.project_id || p.id}</span>
<span className={`badge ${(p.level === 'edit') ? 'accent' : 'neutral'}`}>{p.level || 'view'}</span>
<span className="badge neutral" title={isGroup ? 'Inherited from group ' + viaLabel : 'Granted directly'}>
<Icon name={isGroup ? 'users' : 'user'} size={9} /> {viaLabel}
</span>
</div>
);
})}
</div>
{/* Group memberships */}
<div>
<div style={{ fontSize: 11, color: 'var(--text-3)', textTransform: 'uppercase', letterSpacing: '0.06em', fontWeight: 600, marginBottom: 8 }}>
Groups ({memberships.length})
</div>
{memberships.length === 0 && (
<div style={{ fontSize: 12, color: 'var(--text-4)' }}>Not a member of any group.</div>
)}
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
{memberships.map(g => (
<span key={g.id || g.group_id || g.name} className="badge neutral" style={{ display: 'inline-flex', alignItems: 'center', gap: 5 }}>
<Icon name="users" size={9} />{g.name || g.group_name || g.group_id}
</span>
))}
</div>
</div>
</div>
)}
</div>
)}
</div>
);
}
function EditUserModal({ user, onClose, onSaved }) { function EditUserModal({ user, onClose, onSaved }) {
const [name, setName] = React.useState(user.display_name || user.name || ''); const [name, setName] = React.useState(user.display_name || user.name || '');
const [saving, setSaving] = React.useState(false); const [saving, setSaving] = React.useState(false);
@ -424,6 +619,7 @@ function GroupsPanel({ groups, users, onChange }) {
const [newDesc, setNewDesc] = React.useState(''); const [newDesc, setNewDesc] = React.useState('');
const [expandedId, setExpandedId] = React.useState(null); const [expandedId, setExpandedId] = React.useState(null);
const [members, setMembers] = React.useState({}); // groupId -> [user] const [members, setMembers] = React.useState({}); // groupId -> [user]
const [confirm, confirmModal] = window.useConfirm();
const createGroup = () => { const createGroup = () => {
if (!newName.trim()) return; if (!newName.trim()) return;
@ -432,8 +628,8 @@ function GroupsPanel({ groups, users, onChange }) {
.catch(e => alert('Create failed: ' + e.message)); .catch(e => alert('Create failed: ' + e.message));
}; };
const deleteGroup = (g) => { const deleteGroup = async (g) => {
if (!confirm(`Delete group "${g.name}"?`)) return; if (!(await confirm({ title: 'Delete group?', message: `Delete group "${g.name}"?` }))) return;
window.ZAMPP_API.fetch('/groups/' + g.id, { method: 'DELETE' }) window.ZAMPP_API.fetch('/groups/' + g.id, { method: 'DELETE' })
.then(onChange) .then(onChange)
.catch(e => alert('Delete failed: ' + e.message)); .catch(e => alert('Delete failed: ' + e.message));
@ -468,6 +664,7 @@ function GroupsPanel({ groups, users, onChange }) {
return ( return (
<div> <div>
{confirmModal}
<div style={{ display: 'flex', alignItems: 'center', marginBottom: 12 }}> <div style={{ display: 'flex', alignItems: 'center', marginBottom: 12 }}>
<div style={{ flex: 1, fontSize: 12, color: 'var(--text-3)' }}> <div style={{ flex: 1, fontSize: 12, color: 'var(--text-3)' }}>
Groups let you bundle users for project access. Memberships are checked when role-based access is enforced. Groups let you bundle users for project access. Memberships are checked when role-based access is enforced.
@ -830,6 +1027,7 @@ function Containers() {
const [containers, setContainers] = React.useState(null); const [containers, setContainers] = React.useState(null);
const [restartFlashState, setRestartFlashState] = React.useState(null); const [restartFlashState, setRestartFlashState] = React.useState(null);
const [logsModalState, setLogsModalState] = React.useState(null); const [logsModalState, setLogsModalState] = React.useState(null);
const [confirm, confirmModal] = window.useConfirm();
// #111 - guard restart-flash timers against unmount. // #111 - guard restart-flash timers against unmount.
const mountedRef = React.useRef(true); const mountedRef = React.useRef(true);
const flashTimerRef = React.useRef(null); const flashTimerRef = React.useRef(null);
@ -860,8 +1058,8 @@ function Containers() {
const showLogs = (c) => setLogsModal(c); const showLogs = (c) => setLogsModal(c);
const restartContainer = (c) => { const restartContainer = async (c) => {
if (!window.confirm('Restart container "' + c.name + '"?\nIn-flight requests will be dropped.')) return; if (!(await confirm({ title: 'Restart container?', message: 'Restart container "' + c.name + '"?\nIn-flight requests will be dropped.', confirmLabel: 'Restart' }))) return;
setRestartFlashSafe({ name: c.name, status: 'pending' }); setRestartFlashSafe({ name: c.name, status: 'pending' });
window.ZAMPP_API.fetch('/cluster/containers/' + encodeURIComponent(c.id || c.name) + '/restart', { method: 'POST' }) window.ZAMPP_API.fetch('/cluster/containers/' + encodeURIComponent(c.id || c.name) + '/restart', { method: 'POST' })
.then(() => { .then(() => {
@ -878,6 +1076,7 @@ function Containers() {
return ( return (
<div className="page"> <div className="page">
{confirmModal}
<div className="page-header"> <div className="page-header">
<h1>Containers</h1> <h1>Containers</h1>
<span className="subtitle">Docker Compose services across the cluster</span> <span className="subtitle">Docker Compose services across the cluster</span>
@ -976,7 +1175,11 @@ function Containers() {
<span>{(c.cpu || 0).toFixed(1)}%</span> <span>{(c.cpu || 0).toFixed(1)}%</span>
</div> </div>
</div> </div>
<div className="mono" style={{ fontSize: 11.5 }}>{c.mem} MB</div> <div className="mono" style={{ fontSize: 11.5 }}>
{c.memBytes != null
? `${Math.round(c.memBytes / 1048576)} MB`
: "N/A"}
</div>
<div className="mono" style={{ fontSize: 10.5, color: "var(--text-3)" }}>{c.ports}</div> <div className="mono" style={{ fontSize: 10.5, color: "var(--text-3)" }}>{c.ports}</div>
<div style={{ display: "flex", gap: 4 }}> <div style={{ display: "flex", gap: 4 }}>
<button className="btn ghost sm" onClick={() => showLogs(c)}>Logs</button> <button className="btn ghost sm" onClick={() => showLogs(c)}>Logs</button>
@ -992,6 +1195,148 @@ function Containers() {
} }
// //
// DriverPanel - "Capture Drivers / SDKs" section inside the node detail panel.
// Per vendor (Blackmagic / AJA / Deltacast / NDI): shows detected status from
// GET /cluster/:id/driver-status (host probe) cross-checked with heartbeat
// capabilities, plus an "Install / Update" button that POSTs
// /cluster/:id/install-driver {vendor} and streams the agent log into a live
// output area, surfacing success/failure + "reboot required".
const DRIVER_VENDORS = [
{ key: 'blackmagic', label: 'Blackmagic', hint: 'Desktop Video driver (.deb)' },
{ key: 'aja', label: 'AJA', hint: 'NTV2 driver / SDK' },
{ key: 'deltacast', label: 'Deltacast', hint: 'VideoMaster installer' },
{ key: 'ndi', label: 'NDI', hint: 'Redistributable runtime libs' },
];
function DriverPanel({ sel }) {
const [status, setStatus] = React.useState(null); // { kernel, vendors:{...} }
const [loading, setLoading] = React.useState(true);
const [statusErr, setStatusErr] = React.useState(null);
const [busy, setBusy] = React.useState(null); // vendor key currently installing
const [log, setLog] = React.useState(null); // { vendor, text, ok, rebootRequired }
const loadStatus = React.useCallback(() => {
if (!sel.dbId) return;
setLoading(true); setStatusErr(null);
window.ZAMPP_API.fetch(`/cluster/${sel.dbId}/driver-status`)
.then(d => { setStatus(d); setLoading(false); })
.catch(e => { setStatusErr(e.message || 'unreachable'); setLoading(false); });
}, [sel.dbId]);
React.useEffect(() => { loadStatus(); }, [loadStatus]);
// Heartbeat-reported capabilities give a second signal for the two card types
// the cluster already enumerates (Blackmagic ports, Deltacast ports).
const capPresent = (vendor) => {
if (vendor === 'blackmagic') return (sel.bmdPorts || []).length > 0;
if (vendor === 'deltacast') return (sel.deltacastPorts || []).length > 0;
return false;
};
const isInstalled = (vendor) => {
const v = status && status.vendors && status.vendors[vendor];
return (v && v.installed) || capPresent(vendor);
};
const install = (vendor) => {
setBusy(vendor);
setLog({ vendor, text: `[ui] requesting install of ${vendor} on ${sel.id}\n`, ok: null, rebootRequired: false });
// Raw fetch: we need the JSON body (logs) even on a non-2xx response.
fetch(`/api/v1/cluster/${sel.dbId}/install-driver`, {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'dragonflight-ui' },
body: JSON.stringify({ vendor }),
})
.then(async (res) => {
let body = {};
try { body = await res.json(); } catch (_) {}
const text = (body.logs && body.logs.trim())
? body.logs
: (body.error || `Install ${res.ok ? 'completed' : 'failed'} (HTTP ${res.status})`);
setLog({ vendor, text, ok: !!body.ok, rebootRequired: !!body.rebootRequired });
setBusy(null);
loadStatus();
})
.catch((e) => {
setLog({ vendor, text: `[ui] request failed: ${e.message}`, ok: false, rebootRequired: false });
setBusy(null);
});
};
return (
<div style={{ marginTop: 10 }}>
<div style={{ fontSize: 11, color: "var(--text-3)", marginBottom: 6, display: "flex", alignItems: "center", gap: 6 }}>
<Icon name="download" size={11} />
Capture Drivers / SDKs
{status && status.kernel && (
<span style={{ marginLeft: "auto", fontSize: 10, color: "var(--text-4)", fontFamily: "var(--font-mono)" }}>
kernel {status.kernel}
</span>
)}
</div>
{statusErr && (
<div style={{ fontSize: 11, color: "var(--danger)", marginBottom: 6 }}>
Driver status unavailable: {statusErr}
</div>
)}
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
{DRIVER_VENDORS.map(v => {
const installed = isInstalled(v.key);
const installing = busy === v.key;
return (
<div key={v.key} style={{
display: "flex", alignItems: "center", gap: 8,
padding: "6px 10px", background: "var(--bg-2)", borderRadius: 5,
border: "1px solid var(--border)",
}}>
<div style={{ minWidth: 0 }}>
<div style={{ fontSize: 12, fontWeight: 600, color: "var(--text-1)" }}>{v.label}</div>
<div style={{ fontSize: 10.5, color: "var(--text-4)" }}>{v.hint}</div>
</div>
<span style={{
marginLeft: "auto", fontSize: 10, fontWeight: 600, padding: "2px 6px", borderRadius: 3,
background: installed ? "rgba(91,250,138,0.15)" : "rgba(255,255,255,0.05)",
color: installed ? "var(--success)" : "var(--text-3)",
}}>
{loading ? "…" : (installed ? "INSTALLED" : "NOT INSTALLED")}
</span>
<button className="btn ghost sm" disabled={installing || !!busy}
onClick={() => install(v.key)}>
{installing ? "Installing…" : (installed ? "Update" : "Install")}
</button>
</div>
);
})}
</div>
{log && (
<div style={{ marginTop: 8 }}>
<div style={{ fontSize: 10.5, marginBottom: 4, display: "flex", alignItems: "center", gap: 6 }}>
<span style={{ color: "var(--text-3)" }}>{log.vendor} install output</span>
{log.ok === true && <span style={{ color: "var(--success)", fontWeight: 700 }}>SUCCESS</span>}
{log.ok === false && <span style={{ color: "var(--danger)", fontWeight: 700 }}>FAILED</span>}
{log.rebootRequired && (
<span style={{ marginLeft: "auto", fontSize: 10, fontWeight: 700, color: "var(--warning, #f0a500)" }}>
REBOOT REQUIRED
</span>
)}
</div>
<pre style={{
margin: 0, maxHeight: 180, overflow: "auto",
fontSize: 10.5, lineHeight: 1.4, fontFamily: "var(--font-mono)",
color: "var(--text-2)", background: "var(--bg-1)",
border: "1px solid var(--border)", borderRadius: 4, padding: "6px 8px",
whiteSpace: "pre-wrap", wordBreak: "break-word",
}}>{log.text}</pre>
</div>
)}
</div>
);
}
// BmdCardPanel - capture-card section inside the Cluster node detail panel. // BmdCardPanel - capture-card section inside the Cluster node detail panel.
// Shows port chips with live video-presence dots AND the BMD SVG card diagram. // Shows port chips with live video-presence dots AND the BMD SVG card diagram.
// //
@ -1111,6 +1456,7 @@ function Cluster() {
const [hovered, setHovered] = React.useState(null); const [hovered, setHovered] = React.useState(null);
// Map of "node_id:portIndex" signal entry from /cluster/devices/blackmagic/signal // Map of "node_id:portIndex" signal entry from /cluster/devices/blackmagic/signal
const [portSignals, setPortSignals] = React.useState({}); const [portSignals, setPortSignals] = React.useState({});
const [confirm, confirmModal] = window.useConfirm();
const refresh = React.useCallback(() => { const refresh = React.useCallback(() => {
window.ZAMPP_API.fetch('/cluster') window.ZAMPP_API.fetch('/cluster')
@ -1195,8 +1541,8 @@ function Cluster() {
commands: [`ssh ${node.ip || node.id} 'docker stop node-agent'`], commands: [`ssh ${node.ip || node.id} 'docker stop node-agent'`],
}); });
const removeNode = (node) => { const removeNode = async (node) => {
if (!window.confirm('Remove node ' + node.id + ' from the cluster?\nThis does not stop the machine: it only removes it from cluster membership.')) return; if (!(await confirm({ title: 'Remove node?', message: 'Remove node ' + node.id + ' from the cluster?\nThis does not stop the machine: it only removes it from cluster membership.', confirmLabel: 'Remove' }))) return;
window.ZAMPP_API.fetch('/cluster/nodes/' + encodeURIComponent(node.dbId || node.id), { method: 'DELETE' }) window.ZAMPP_API.fetch('/cluster/nodes/' + encodeURIComponent(node.dbId || node.id), { method: 'DELETE' })
.then(() => refresh()) .then(() => refresh())
.catch(e => setAdviceModal({ title: 'Remove failed', lines: [e.message] })); .catch(e => setAdviceModal({ title: 'Remove failed', lines: [e.message] }));
@ -1210,6 +1556,7 @@ function Cluster() {
return ( return (
<div className="page"> <div className="page">
{confirmModal}
<div className="page-header"> <div className="page-header">
<h1>Cluster</h1> <h1>Cluster</h1>
<span className="subtitle">{NODES.filter(n => n.status === "online").length} of {NODES.length} nodes online</span> <span className="subtitle">{NODES.filter(n => n.status === "online").length} of {NODES.length} nodes online</span>
@ -1365,6 +1712,23 @@ function Cluster() {
</div> </div>
)} )}
{g.device && <div style={{ fontSize: 10.5, color: "var(--text-4)", fontFamily: "var(--font-mono)" }}>{g.device}</div>} {g.device && <div style={{ fontSize: 10.5, color: "var(--text-4)", fontFamily: "var(--font-mono)" }}>{g.device}</div>}
{/* Issue #166 — live NVENC/GPU encode telemetry (0 until a live encode runs) */}
{(g.utilPct != null || g.encUtilPct != null || g.nvencSessions != null) && (
<div style={{ display: "flex", flexWrap: "wrap", gap: "4px 10px", marginTop: 4, fontSize: 10.5, fontFamily: "var(--font-mono)" }}>
{g.utilPct != null && (
<span style={{ color: "var(--text-3)" }}>GPU <strong style={{ color: "var(--text-2)" }}>{g.utilPct}%</strong></span>
)}
{g.encUtilPct != null && (
<span style={{ color: "var(--text-3)" }}>ENC <strong style={{ color: g.encUtilPct > 0 ? "var(--success)" : "var(--text-2)" }}>{g.encUtilPct}%</strong></span>
)}
{g.memUsedMb != null && g.memMb && (
<span style={{ color: "var(--text-3)" }}>VRAM <strong style={{ color: "var(--text-2)" }}>{g.memUsedMb}/{g.memMb} MB</strong></span>
)}
{g.nvencSessions != null && (
<span style={{ color: "var(--text-3)" }}>NVENC <strong style={{ color: g.nvencSessions > 0 ? "var(--success)" : "var(--text-2)" }}>{g.nvencSessions}</strong></span>
)}
</div>
)}
</div> </div>
<span style={{ <span style={{
fontSize: 10, fontWeight: 600, padding: "2px 6px", borderRadius: 3, fontSize: 10, fontWeight: 600, padding: "2px 6px", borderRadius: 3,
@ -1379,6 +1743,9 @@ function Cluster() {
{/* ── Capture cards ── */} {/* ── Capture cards ── */}
<BmdCardPanel sel={sel} portSignals={portSignals} /> <BmdCardPanel sel={sel} portSignals={portSignals} />
{/* ── Capture Drivers / SDKs ── */}
<DriverPanel sel={sel} />
<div style={{ display: "flex", gap: 6, marginTop: 6 }}> <div style={{ display: "flex", gap: 6, marginTop: 6 }}>
<button className="btn ghost sm" onClick={() => nodeLogsHint(sel)}>Logs</button> <button className="btn ghost sm" onClick={() => nodeLogsHint(sel)}>Logs</button>
<button className="btn ghost sm" onClick={() => drainNode(sel)}>Drain</button> <button className="btn ghost sm" onClick={() => drainNode(sel)}>Drain</button>
@ -1420,23 +1787,15 @@ function Cluster() {
); );
} }
// AddNodeModal Approach A onboarding wizard. Collects a node name + role, // AddNodeModal Approach A onboarding wizard. Collects a node name, mints a
// mints a one-time auth token via /auth/tokens, and renders a ready-to-paste // one-time auth token via /auth/tokens, and renders a ready-to-paste
// `curl | bash` command that provisions the machine via deploy/onboard-node.sh. // `curl | bash` command that provisions the machine via deploy/onboard-node.sh.
// //
// Role compose PROFILES mapping (see docker-compose.worker.yml): // No role picker: the new node self-detects its hardware (GPU / DeckLink /
// Worker "worker" // Deltacast) in onboard-node.sh and auto-enables the matching compose profiles
// Capture "worker capture" // (worker always; + gpu / + capture when present). Zero manual choice.
// GPU "worker gpu" (worker-l4 service, profiles: [gpu])
const ADD_NODE_ROLES = [
{ id: 'worker', label: 'Worker', profiles: 'worker', desc: 'CPU transcode / general jobs' },
{ id: 'capture', label: 'Capture', profiles: 'worker capture', desc: 'SDI / DeckLink ingest' },
{ id: 'gpu', label: 'GPU', profiles: 'worker gpu', desc: 'NVENC-accelerated transcode' },
];
function AddNodeModal({ onClose }) { function AddNodeModal({ onClose }) {
const [nodeName, setNodeName] = React.useState(''); const [nodeName, setNodeName] = React.useState('');
const [role, setRole] = React.useState('worker');
const [apiUrl, setApiUrl] = React.useState(''); const [apiUrl, setApiUrl] = React.useState('');
const [info, setInfo] = React.useState(null); // { scriptUrl, branch } const [info, setInfo] = React.useState(null); // { scriptUrl, branch }
const [command, setCommand] = React.useState(null); // generated string const [command, setCommand] = React.useState(null); // generated string
@ -1454,8 +1813,6 @@ function AddNodeModal({ onClose }) {
.catch(() => {}); // leave apiUrl empty user must fill it before Generate .catch(() => {}); // leave apiUrl empty user must fill it before Generate
}, []); }, []);
const roleDef = ADD_NODE_ROLES.find(r => r.id === role) || ADD_NODE_ROLES[0];
const generate = async () => { const generate = async () => {
setError(null); setError(null);
if (!nodeName.trim()) { setError('Node name is required.'); return; } if (!nodeName.trim()) { setError('Node name is required.'); return; }
@ -1477,8 +1834,7 @@ function AddNodeModal({ onClose }) {
const scriptUrl = (info && info.scriptUrl) const scriptUrl = (info && info.scriptUrl)
|| 'https://forge.wilddragon.net/zgaetano/wild-dragon/raw/branch/main/deploy/onboard-node.sh'; || 'https://forge.wilddragon.net/zgaetano/wild-dragon/raw/branch/main/deploy/onboard-node.sh';
const cmd = const cmd =
`curl -sL ${scriptUrl} | NODE_TOKEN=${token} MAM_API_URL=${apiUrl.trim()} ` + `curl -sL ${scriptUrl} | NODE_TOKEN=${token} MAM_API_URL=${apiUrl.trim()} bash`;
`NODE_ROLE=${role} PROFILES="${roleDef.profiles}" bash`;
setCommand(cmd); setCommand(cmd);
} catch (e) { } catch (e) {
setError(e.message || 'Network error'); setError(e.message || 'Network error');
@ -1511,21 +1867,6 @@ function AddNodeModal({ onClose }) {
value={nodeName} onChange={e => setNodeName(e.target.value)} /> value={nodeName} onChange={e => setNodeName(e.target.value)} />
</div> </div>
<div style={{ marginBottom: 14 }}>
<label style={{ display: 'block', fontSize: 11.5, color: 'var(--text-3)', marginBottom: 5 }}>Role</label>
<div style={{ display: 'flex', gap: 6 }}>
{ADD_NODE_ROLES.map(rd => (
<button key={rd.id}
className={'btn sm' + (role === rd.id ? ' primary' : ' ghost')}
style={{ flex: 1, flexDirection: 'column', alignItems: 'flex-start', gap: 2, padding: '8px 10px' }}
onClick={() => setRole(rd.id)}>
<span style={{ fontWeight: 600 }}>{rd.label}</span>
<span style={{ fontSize: 10, opacity: 0.8 }}>{rd.desc}</span>
</button>
))}
</div>
</div>
<div style={{ marginBottom: 4 }}> <div style={{ marginBottom: 4 }}>
<label style={{ display: 'block', fontSize: 11.5, color: 'var(--text-3)', marginBottom: 5 }}>Primary API URL</label> <label style={{ display: 'block', fontSize: 11.5, color: 'var(--text-3)', marginBottom: 5 }}>Primary API URL</label>
<input className="field-input mono" style={{ width: '100%', fontSize: 12 }} <input className="field-input mono" style={{ width: '100%', fontSize: 12 }}
@ -1546,6 +1887,9 @@ function AddNodeModal({ onClose }) {
</div> </div>
</div> </div>
<code className="mono" style={{ display: 'block', background: 'var(--bg-2)', padding: 12, borderRadius: 6, fontSize: 11.5, lineHeight: 1.5, wordBreak: 'break-all', whiteSpace: 'pre-wrap' }}>{command}</code> <code className="mono" style={{ display: 'block', background: 'var(--bg-2)', padding: 12, borderRadius: 6, fontSize: 11.5, lineHeight: 1.5, wordBreak: 'break-all', whiteSpace: 'pre-wrap' }}>{command}</code>
<div style={{ fontSize: 11, color: 'var(--text-4)', marginTop: 8, lineHeight: 1.5 }}>
Profiles (worker / capture / GPU) are auto-selected from the new machine's detected hardware no need to choose.
</div>
<ol style={{ margin: '12px 0 0', paddingLeft: 18, fontSize: 12, color: 'var(--text-2)', lineHeight: 1.6 }}> <ol style={{ margin: '12px 0 0', paddingLeft: 18, fontSize: 12, color: 'var(--text-2)', lineHeight: 1.6 }}>
<li>SSH into the fresh Ubuntu machine.</li> <li>SSH into the fresh Ubuntu machine.</li>
<li>Paste and run this command.</li> <li>Paste and run this command.</li>
@ -2426,6 +2770,7 @@ function SdkVendorRow({ vendor, status, onDone }) {
const fileRef = React.useRef(null); const fileRef = React.useRef(null);
const [uploading, setUploading] = React.useState(false); const [uploading, setUploading] = React.useState(false);
const [progress, setProgress] = React.useState(0); const [progress, setProgress] = React.useState(0);
const [confirm, confirmModal] = window.useConfirm();
const deployed = status && status.file_count > 0; const deployed = status && status.file_count > 0;
const lastUpload = status?.uploaded_at const lastUpload = status?.uploaded_at
@ -2467,8 +2812,8 @@ function SdkVendorRow({ vendor, status, onDone }) {
}); });
}; };
const clear = () => { const clear = async () => {
if (!confirm('Remove staged ' + vendor.name + ' SDK files?')) return; if (!(await confirm({ title: 'Remove staged SDK files?', message: 'Remove staged ' + vendor.name + ' SDK files?', confirmLabel: 'Remove' }))) return;
window.ZAMPP_API.fetch('/sdk/' + vendor.id, { method: 'DELETE' }) window.ZAMPP_API.fetch('/sdk/' + vendor.id, { method: 'DELETE' })
.then(() => onDone(vendor.name + ': cleared.', true)) .then(() => onDone(vendor.name + ': cleared.', true))
.catch(e => onDone(vendor.name + ': ' + e.message, false)); .catch(e => onDone(vendor.name + ': ' + e.message, false));
@ -2476,6 +2821,7 @@ function SdkVendorRow({ vendor, status, onDone }) {
return ( return (
<div style={{ border: '1px solid var(--border)', borderRadius: 6, padding: 12, background: 'var(--bg-2)' }}> <div style={{ border: '1px solid var(--border)', borderRadius: 6, padding: 12, background: 'var(--bg-2)' }}>
{confirmModal}
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
<strong style={{ fontSize: 13 }}>{vendor.name}</strong> <strong style={{ fontSize: 13 }}>{vendor.name}</strong>
{deployed {deployed

View file

@ -23,6 +23,7 @@ function AssetDetail({ asset, onClose }) {
const [comments, setComments] = React.useState([]); const [comments, setComments] = React.useState([]);
const [newComment, setNewComment] = React.useState(""); const [newComment, setNewComment] = React.useState("");
const [commentsLoading, setCommentsLoading] = React.useState(false); const [commentsLoading, setCommentsLoading] = React.useState(false);
const [confirm, confirmModal] = window.useConfirm();
// Stream / video state // Stream / video state
const [streamUrl, setStreamUrl] = React.useState(null); const [streamUrl, setStreamUrl] = React.useState(null);
@ -198,6 +199,45 @@ function AssetDetail({ asset, onClose }) {
// Pull a presigned hi-res URL and trigger a browser download with the // Pull a presigned hi-res URL and trigger a browser download with the
// asset's display name as the filename. Falls back to opening in a new tab. // asset's display name as the filename. Falls back to opening in a new tab.
const [downloading, setDownloading] = React.useState(false); const [downloading, setDownloading] = React.useState(false);
// Gate the download behind a one-time "large file / connection speed"
// warning, shared with the library via the df.lib.download.warnDismissed
// localStorage flag. Once dismissed, downloads start without the prompt.
const dismissForeverRef = React.useRef(false);
const requestDownload = async function() {
if (downloading) return;
let dismissed = false;
try { dismissed = localStorage.getItem('df.lib.download.warnDismissed') === '1'; } catch (_) {}
if (!dismissed) {
dismissForeverRef.current = false;
const ok = await confirm({
title: 'Download original',
message: <div>
<div style={{ fontSize: 13, color: 'var(--text-2)', lineHeight: 1.5, marginBottom: 10 }}>
You're about to download the full-length original master for <b>{asset.name}</b>.
These files can be very large and download speed depends on your connection.
</div>
<label style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 12.5, color: 'var(--text-3)', cursor: 'pointer' }}>
<input
type="checkbox"
onChange={function(e) { dismissForeverRef.current = e.target.checked; }}
/>
Don't show this warning again
</label>
</div>,
confirmLabel: 'Download',
cancelLabel: 'Cancel',
danger: false,
});
if (!ok) return;
// Persist the dismissal only after the user confirms the download.
if (dismissForeverRef.current) {
try { localStorage.setItem('df.lib.download.warnDismissed', '1'); } catch (_) {}
}
}
downloadHires();
};
const downloadHires = function() { const downloadHires = function() {
if (downloading) return; if (downloading) return;
setDownloading(true); setDownloading(true);
@ -231,9 +271,12 @@ function AssetDetail({ asset, onClose }) {
if (navigator.clipboard) navigator.clipboard.writeText(assetId).catch(function() {}); if (navigator.clipboard) navigator.clipboard.writeText(assetId).catch(function() {});
setMenuOpen(false); setMenuOpen(false);
}; };
const deleteAsset = function() { const deleteAsset = async function() {
setMenuOpen(false); setMenuOpen(false);
if (!window.confirm('Delete "' + (asset.name || assetId) + '" permanently?\nRemoves DB row + S3 objects. Cannot be undone.')) return; if (!(await confirm({
title: 'Delete asset?',
message: 'Delete "' + (asset.name || assetId) + '" permanently?\nRemoves DB row + S3 objects. Cannot be undone.',
}))) return;
window.ZAMPP_API.fetch('/assets/' + assetId + '?hard=true', { method: 'DELETE' }) window.ZAMPP_API.fetch('/assets/' + assetId + '?hard=true', { method: 'DELETE' })
.then(function() { onClose && onClose(); }) .then(function() { onClose && onClose(); })
.catch(function(e) { window.alert('Delete failed: ' + e.message); }); .catch(function(e) { window.alert('Delete failed: ' + e.message); });
@ -325,8 +368,8 @@ function AssetDetail({ asset, onClose }) {
.catch(function() {}); .catch(function() {});
}; };
const deleteComment = function(c) { const deleteComment = async function(c) {
if (!confirm('Delete this comment?')) return; if (!(await confirm({ title: 'Delete comment?', message: 'Delete this comment?' }))) return;
window.ZAMPP_API.fetch('/assets/' + assetId + '/comments/' + c.id, { method: 'DELETE' }) window.ZAMPP_API.fetch('/assets/' + assetId + '/comments/' + c.id, { method: 'DELETE' })
.then(function() { .then(function() {
setComments(function(prev) { return prev.filter(x => x.id !== c.id); }); setComments(function(prev) { return prev.filter(x => x.id !== c.id); });
@ -355,6 +398,7 @@ function AssetDetail({ asset, onClose }) {
return ( return (
<div className="asset-detail fade-in"> <div className="asset-detail fade-in">
{confirmModal}
<div className="asset-detail-header"> <div className="asset-detail-header">
<button className="icon-btn" aria-label="Back" onClick={onClose}><Icon name="arrowLeft" /></button> <button className="icon-btn" aria-label="Back" onClick={onClose}><Icon name="arrowLeft" /></button>
<div style={{ display: "flex", flexDirection: "column", minWidth: 0 }}> <div style={{ display: "flex", flexDirection: "column", minWidth: 0 }}>
@ -367,9 +411,11 @@ function AssetDetail({ asset, onClose }) {
</div> </div>
</div> </div>
<div style={{ flex: 1 }} /> <div style={{ flex: 1 }} />
<button className="btn ghost sm" onClick={downloadHires} disabled={downloading} title="Download the hi-res master file"> {asset.original_s3_key && (
<Icon name="download" />{downloading ? 'Preparing…' : 'Download'} <button className="btn ghost sm" onClick={requestDownload} disabled={downloading} title="Download the hi-res master file">
</button> <Icon name="download" />{downloading ? 'Preparing…' : 'Download'}
</button>
)}
<div style={{ position: 'relative' }}> <div style={{ position: 'relative' }}>
<button ref={moreBtnRef} className="icon-btn" aria-label="More actions" onClick={function(e) { e.stopPropagation(); setMenuOpen(function(v) { return !v; }); }}> <button ref={moreBtnRef} className="icon-btn" aria-label="More actions" onClick={function(e) { e.stopPropagation(); setMenuOpen(function(v) { return !v; }); }}>
<Icon name="more" /> <Icon name="more" />

View file

@ -7,6 +7,7 @@ function Editor() {
const [projectId, setProjectId] = React.useState(null); const [projectId, setProjectId] = React.useState(null);
const [sequences, setSequences] = React.useState([]); const [sequences, setSequences] = React.useState([]);
const [currentSeq, setCurrentSeq] = React.useState(null); const [currentSeq, setCurrentSeq] = React.useState(null);
const [confirm, confirmModal] = window.useConfirm();
const [assets, setAssets] = React.useState([]); const [assets, setAssets] = React.useState([]);
const [bins, setBins] = React.useState([]); const [bins, setBins] = React.useState([]);
const [sourceAsset, setSourceAsset] = React.useState(null); const [sourceAsset, setSourceAsset] = React.useState(null);
@ -159,7 +160,7 @@ function Editor() {
async function deleteSequence() { async function deleteSequence() {
if (!currentSeq) return; if (!currentSeq) return;
if (!window.confirm('Delete sequence "' + currentSeq.name + '"? This cannot be undone.')) return; if (!(await confirm({ title: 'Delete sequence?', message: 'Delete sequence "' + currentSeq.name + '"? This cannot be undone.' }))) return;
try { try {
await window.ZAMPP_API.deleteSequence(currentSeq.id); await window.ZAMPP_API.deleteSequence(currentSeq.id);
const remaining = sequences.filter(s => s.id !== currentSeq.id); const remaining = sequences.filter(s => s.id !== currentSeq.id);
@ -377,6 +378,7 @@ function Editor() {
return ( return (
<div className="editor-shell" style={{ position: 'relative', display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}> <div className="editor-shell" style={{ position: 'relative', display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
{confirmModal}
{/* Beta banner: flat strip across top, no glassmorphism or gradient. */} {/* Beta banner: flat strip across top, no glassmorphism or gradient. */}
<div className="editor-beta-banner"> <div className="editor-beta-banner">

View file

@ -23,12 +23,19 @@ function Home({ navigate }) {
// Pull live counts so the tile subtitles ("34 assets", "0 live", "3 running") // Pull live counts so the tile subtitles ("34 assets", "0 live", "3 running")
// reflect what's actually in the DB right now, not a stale boot-time cache. // reflect what's actually in the DB right now, not a stale boot-time cache.
const [cards, setCards] = React.useState({}); const [cards, setCards] = React.useState({});
// Playout has no /metrics/home card yet (and the playout schema may not be
// migrated on every install); fetch /playout/channels separately and degrade
// silently the tile just shows "No channels" if the endpoint isn't there.
const [playoutChannels, setPlayoutChannels] = React.useState(null);
React.useEffect(() => { React.useEffect(() => {
let cancelled = false; let cancelled = false;
const load = () => { const load = () => {
window.ZAMPP_API.fetch('/metrics/home?hours=1') window.ZAMPP_API.fetch('/metrics/home?hours=1')
.then(d => { if (!cancelled) setCards(d?.cards || {}); }) .then(d => { if (!cancelled) setCards(d?.cards || {}); })
.catch(() => {}); .catch(() => {});
window.ZAMPP_API.fetch('/playout/channels')
.then(d => { if (!cancelled) setPlayoutChannels(Array.isArray(d) ? d : []); })
.catch(() => { if (!cancelled) setPlayoutChannels([]); });
}; };
load(); load();
const t = setInterval(load, 30_000); const t = setInterval(load, 30_000);
@ -67,17 +74,24 @@ function Home({ navigate }) {
id: 'playout', id: 'playout',
label: 'Playout', label: 'Playout',
icon: 'signal', icon: 'signal',
tone: 'live', tone: 'accent',
sub: 'Master Control', sub: (() => {
desc: 'Play assets to SDI, NDI, SRT or RTMP via CasparCG.', if (playoutChannels === null) return '·';
const total = playoutChannels.length;
const onAir = playoutChannels.filter(c => c.status === 'running').length;
if (total === 0) return 'No channels';
if (onAir > 0) return onAir + ' on air · ' + total + ' channel' + (total === 1 ? '' : 's');
return total + ' channel' + (total === 1 ? '' : 's');
})(),
desc: 'Master Control. SDI · NDI · SRT · RTMP playout, playlists, as-run.',
}, },
{ {
id: '__downloads', id: '__downloads',
label: 'Downloads', label: 'Downloads',
icon: 'download', icon: 'download',
tone: 'purple', tone: 'purple',
sub: 'Premiere panel · Dragon-ISO', sub: 'Plugin · Teams ISO',
desc: 'Download the Premiere Pro panel and Dragon-ISO NDI tools.', desc: 'Download the Premiere Pro UXP plugin and the Teams ISO installer.',
}, },
{ {
id: 'jobs', id: 'jobs',
@ -270,14 +284,20 @@ function Home({ navigate }) {
</span> </span>
)} )}
</div> </div>
<div className="launcher-footer">Created by Wild Dragon LLC</div>
</div> </div>
{showDownloads && <DownloadsModal onClose={() => setShowDownloads(false)} />} {showDownloads && <DownloadsModal onClose={() => setShowDownloads(false)} />}
</div> </div>
); );
} }
// Combined downloads modal: Premiere Pro panel + Dragon-ISO. // Modal listing all downloads: the Premiere Pro UXP plugin (.ccx, one per
// released version, sourced from window.PREMIERE_RELEASES written by the
// Settings SDKs section in screens-admin.jsx) plus the Teams ISO installer
// (window.TEAMS_ISO; the .exe slot is wired but the file may still be pending).
function DownloadsModal({ onClose }) { function DownloadsModal({ onClose }) {
const teamsIso = window.TEAMS_ISO || {};
const releases = (window.PREMIERE_RELEASES || []).slice().sort((a, b) => { const releases = (window.PREMIERE_RELEASES || []).slice().sort((a, b) => {
const av = String(a.version || ''), bv = String(b.version || ''); const av = String(a.version || ''), bv = String(b.version || '');
return bv.localeCompare(av, undefined, { numeric: true }); return bv.localeCompare(av, undefined, { numeric: true });
@ -293,20 +313,37 @@ function DownloadsModal({ onClose }) {
<div> <div>
<div style={{ fontSize: 15, fontWeight: 600 }}>Downloads</div> <div style={{ fontSize: 15, fontWeight: 600 }}>Downloads</div>
<div style={{ fontSize: 12, color: 'var(--text-3)', marginTop: 2 }}> <div style={{ fontSize: 12, color: 'var(--text-3)', marginTop: 2 }}>
Premiere Pro panel and Dragon-ISO NDI tools. The Premiere Pro (UXP) plugin and the Teams ISO installer. Install the .ccx per workstation via the Adobe UXP Developer Tool, or double-click it with Creative Cloud installed.
</div> </div>
</div> </div>
<button className="icon-btn" aria-label="Close" onClick={onClose}><Icon name="x" /></button> <button className="icon-btn" aria-label="Close" onClick={onClose}><Icon name="x" /></button>
</div> </div>
<div className="modal-body"> <div className="modal-body">
{/* ── Premiere panel ── */} <div className="premiere-release">
<div className="downloads-section-head"> <div className="premiere-release-head">
<Icon name="editor" size={13} /> <span className="premiere-release-version mono">Teams ISO</span>
<span>Premiere Pro panel (UXP)</span> {teamsIso.version && (
</div> <span className="premiere-release-date mono">v{teamsIso.version}</span>
<div style={{ fontSize: 12, color: 'var(--text-3)', marginBottom: 10 }}> )}
Install the .ccx per workstation via the Adobe UXP Developer Tool, or double-click it with Creative Cloud installed. </div>
<div className="premiere-release-notes">
Windows installer for the Teams ISO workstation build.
</div>
<div className="premiere-release-actions">
{teamsIso.available && teamsIso.url ? (
<a href={teamsIso.url} download className="btn primary sm">
<Icon name="download" />Teams ISO (.exe)
</a>
) : (
<>
<span className="btn primary sm" aria-disabled="true" style={{ opacity: 0.5, pointerEvents: 'none' }}>
<Icon name="download" />Teams ISO (.exe)
</span>
<span style={{ fontSize: 11.5, color: 'var(--text-3)' }}>coming soon file pending</span>
</>
)}
</div>
</div> </div>
{releases.length === 0 && ( {releases.length === 0 && (
<div style={{ padding: '12px 0', color: 'var(--text-3)', fontSize: 12 }}> <div style={{ padding: '12px 0', color: 'var(--text-3)', fontSize: 12 }}>

View file

@ -587,6 +587,7 @@ function RecorderRow({ recorder: initialRecorder, onRefresh }) {
const [clipName, setClipName] = React.useState(''); const [clipName, setClipName] = React.useState('');
// Project override for this take. Defaults to the recorder's configured project. // Project override for this take. Defaults to the recorder's configured project.
const [takeProjectId, setTakeProjectId] = React.useState(initialRecorder.project_id || PROJECTS[0]?.id || ''); const [takeProjectId, setTakeProjectId] = React.useState(initialRecorder.project_id || PROJECTS[0]?.id || '');
const [confirm, confirmModal] = window.useConfirm();
const isRec = recorder.status === 'recording'; const isRec = recorder.status === 'recording';
// Keep takeProjectId in sync if the recorder row changes (e.g. after a refresh). // Keep takeProjectId in sync if the recorder row changes (e.g. after a refresh).
@ -657,8 +658,8 @@ function RecorderRow({ recorder: initialRecorder, onRefresh }) {
.catch(e => { setPending(false); setErr(e.message || 'Failed'); setRecorder(initialRecorder); }); .catch(e => { setPending(false); setErr(e.message || 'Failed'); setRecorder(initialRecorder); });
}; };
const handleDelete = () => { const handleDelete = async () => {
if (!window.confirm('Delete recorder "' + recorder.name + '"?\nThis will stop any active recording and cannot be undone.')) return; if (!(await confirm({ title: 'Delete recorder?', message: 'Delete recorder "' + recorder.name + '"?\nThis will stop any active recording and cannot be undone.' }))) return;
window.ZAMPP_API.fetch('/recorders/' + recorder.id, { method: 'DELETE' }) window.ZAMPP_API.fetch('/recorders/' + recorder.id, { method: 'DELETE' })
.then(() => { .then(() => {
onRefresh(); onRefresh();
@ -670,6 +671,7 @@ function RecorderRow({ recorder: initialRecorder, onRefresh }) {
return ( return (
<div className={'recorder-row ' + recorder.status}> <div className={'recorder-row ' + recorder.status}>
{confirmModal}
<div className="recorder-preview"> <div className="recorder-preview">
{isRec && recorder.live_asset_id {isRec && recorder.live_asset_id
? <HlsPreview assetId={recorder.live_asset_id} recorderId={recorder.id} /> ? <HlsPreview assetId={recorder.live_asset_id} recorderId={recorder.id} />
@ -997,6 +999,7 @@ function Capture({ navigate }) {
/* ===== Monitors ===== */ /* ===== Monitors ===== */
function Monitors({ navigate }) { function Monitors({ navigate }) {
const [recorders, setRecorders] = React.useState(window.ZAMPP_DATA?.RECORDERS || []); const [recorders, setRecorders] = React.useState(window.ZAMPP_DATA?.RECORDERS || []);
const [channels, setChannels] = React.useState([]);
const [grid, setGrid] = React.useState(4); const [grid, setGrid] = React.useState(4);
React.useEffect(() => { React.useEffect(() => {
@ -1008,6 +1011,11 @@ function Monitors({ navigate }) {
setRecorders(norm); setRecorders(norm);
}) })
.catch(() => {}); .catch(() => {});
// Playout channels surface here too so an operator can watch on-air
// output alongside ingest. Degrade silently if the endpoint is absent.
window.ZAMPP_API.fetch('/playout/channels')
.then(raw => setChannels(Array.isArray(raw) ? raw : []))
.catch(() => setChannels([]));
}; };
refresh(); refresh();
const id = setInterval(refresh, 5000); const id = setInterval(refresh, 5000);
@ -1032,18 +1040,87 @@ function Monitors({ navigate }) {
</div> </div>
</div> </div>
<div className="page-body"> <div className="page-body">
{feeds.length === 0 ? ( {feeds.length === 0 && channels.length === 0 ? (
<div style={{ padding: 60, textAlign: 'center', color: 'var(--text-3)' }}>No active feeds. Start a recorder to see live video here.</div> <div style={{ padding: 60, textAlign: 'center', color: 'var(--text-3)' }}>No active feeds. Start a recorder or playout channel to see live video here.</div>
) : ( ) : (
<div className="monitors-grid" style={{ display: 'grid', gridTemplateColumns: 'repeat(' + grid + ', 1fr)', gap: 8 }}> <React.Fragment>
{feeds.map((f, i) => <MonitorTile key={f.id} feed={f} seed={i + 1} />)} {feeds.length > 0 && (
</div> <React.Fragment>
<div className="monitor-section-head">Ingest</div>
<div className="monitors-grid" style={{ display: 'grid', gridTemplateColumns: 'repeat(' + grid + ', 1fr)', gap: 8 }}>
{feeds.map((f, i) => <MonitorTile key={f.id} feed={f} seed={i + 1} />)}
</div>
</React.Fragment>
)}
{channels.length > 0 && (
<React.Fragment>
<div className="monitor-section-head">Playout</div>
<div className="monitors-grid" style={{ display: 'grid', gridTemplateColumns: 'repeat(' + grid + ', 1fr)', gap: 8 }}>
{channels.slice(0, grid * grid).map(c => <PlayoutMonitorTile key={c.id} channel={c} />)}
</div>
</React.Fragment>
)}
</React.Fragment>
)} )}
</div> </div>
</div> </div>
); );
} }
function PlayoutMonitorTile({ channel }) {
const videoRef = React.useRef(null);
const hlsRef = React.useRef(null);
const onAir = channel.status === 'running';
const previewUrl = '/api/v1/playout/channels/' + channel.id + '/hls/index.m3u8';
React.useEffect(() => {
const vid = videoRef.current;
if (!vid) return;
if (hlsRef.current) { try { hlsRef.current.destroy(); } catch (_) {} hlsRef.current = null; }
if (!onAir) { vid.src = ''; return; }
if (window.Hls && window.Hls.isSupported()) {
const hls = new window.Hls({
liveSyncDurationCount: 3,
liveMaxLatencyDurationCount: 6,
xhrSetup: (xhr) => { xhr.withCredentials = true; },
});
hlsRef.current = hls;
hls.loadSource(previewUrl);
hls.attachMedia(vid);
hls.on(window.Hls.Events.MANIFEST_PARSED, () => vid.play().catch(() => {}));
} else if (vid.canPlayType('application/vnd.apple.mpegurl')) {
vid.src = previewUrl;
vid.play().catch(() => {});
}
return () => {
if (hlsRef.current) { try { hlsRef.current.destroy(); } catch (_) {} hlsRef.current = null; }
};
}, [onAir, channel.id]);
return (
<div className="monitor-tile">
{onAir ? (
<video ref={videoRef} muted playsInline autoPlay
style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block', background: '#000' }} />
) : (
<FauxFrame />
)}
{onAir && <div style={{ position: 'absolute', inset: 0, border: '2px solid var(--live)', pointerEvents: 'none', borderRadius: 'inherit' }} />}
{!onAir && (
<div style={{ position: 'absolute', inset: 0, display: 'grid', placeItems: 'center', color: 'var(--text-3)', fontSize: 11 }}>channel idle</div>
)}
<div style={{ position: 'absolute', top: 8, left: 8, display: 'flex', gap: 6 }}>
{onAir ? <span className="badge live">ON AIR</span> : <span className="badge neutral">IDLE</span>}
</div>
<div className="monitor-tile-label">
<span className="name">{channel.name}</span>
{channel.output_type && <span className="time mono">{String(channel.output_type).toUpperCase()}</span>}
</div>
</div>
);
}
function MonitorTile({ feed, seed }) { function MonitorTile({ feed, seed }) {
const [levels, setLevels] = React.useState([0.65, 0.78]); const [levels, setLevels] = React.useState([0.65, 0.78]);
const isLive = feed.status === 'recording'; const isLive = feed.status === 'recording';
@ -1525,6 +1602,7 @@ function Schedule({ navigate }) {
const [newDefaults, setNewDefaults] = React.useState(null); const [newDefaults, setNewDefaults] = React.useState(null);
const [editing, setEditing] = React.useState(null); const [editing, setEditing] = React.useState(null);
const [ctxMenu, setCtxMenu] = React.useState(null); // { schedule, x, y } const [ctxMenu, setCtxMenu] = React.useState(null); // { schedule, x, y }
const [confirm, confirmModal] = window.useConfirm();
const [view, setView] = React.useState('today'); // 'today' | 'week' | 'list' const [view, setView] = React.useState('today'); // 'today' | 'week' | 'list'
const [day, setDay] = React.useState(() => _dayStart(new Date())); const [day, setDay] = React.useState(() => _dayStart(new Date()));
const [listFilter, setListFilter] = React.useState('upcoming'); const [listFilter, setListFilter] = React.useState('upcoming');
@ -1601,12 +1679,12 @@ function Schedule({ navigate }) {
}; };
const openNewBlank = () => { setNewDefaults(null); setShowNew(true); }; const openNewBlank = () => { setNewDefaults(null); setShowNew(true); };
const cancel = (s) => { const cancel = async (s) => {
if (!confirm('Cancel scheduled recording "' + s.name + '"?')) return; if (!(await confirm({ title: 'Cancel scheduled recording?', message: 'Cancel scheduled recording "' + s.name + '"?', confirmLabel: 'Cancel recording' }))) return;
window.ZAMPP_API.fetch('/schedules/' + s.id + '/cancel', { method: 'POST' }).then(load).catch(e => alert('Cancel failed: ' + e.message)); window.ZAMPP_API.fetch('/schedules/' + s.id + '/cancel', { method: 'POST' }).then(load).catch(e => alert('Cancel failed: ' + e.message));
}; };
const remove = (s) => { const remove = async (s) => {
if (!confirm('Delete schedule "' + s.name + '"?')) return; if (!(await confirm({ title: 'Delete schedule?', message: 'Delete schedule "' + s.name + '"?' }))) return;
window.ZAMPP_API.fetch('/schedules/' + s.id, { method: 'DELETE' }).then(load).catch(e => alert('Delete failed: ' + e.message)); window.ZAMPP_API.fetch('/schedules/' + s.id, { method: 'DELETE' }).then(load).catch(e => alert('Delete failed: ' + e.message));
}; };
@ -1657,6 +1735,7 @@ function Schedule({ navigate }) {
return ( return (
<div className="epg-page" style={{ '--epg-pph': pph + 'px' }}> <div className="epg-page" style={{ '--epg-pph': pph + 'px' }}>
{confirmModal}
<_StatusStrip schedules={schedules || []} recorders={recorders} now={now} projects={projects} /> <_StatusStrip schedules={schedules || []} recorders={recorders} now={now} projects={projects} />
<div className="epg-toolbar"> <div className="epg-toolbar">

View file

@ -42,6 +42,7 @@ function Jobs({ navigate }) {
const [tab, setTab] = React.useState('all'); const [tab, setTab] = React.useState('all');
const [jobs, setJobs] = React.useState(window.ZAMPP_DATA.JOBS); const [jobs, setJobs] = React.useState(window.ZAMPP_DATA.JOBS);
const [lastFetch, setLastFetch] = React.useState(Date.now()); const [lastFetch, setLastFetch] = React.useState(Date.now());
const [confirm, confirmModal] = window.useConfirm();
const normalizeJob = (j) => { const normalizeJob = (j) => {
const statusMap = { waiting: 'queued', active: 'running', completed: 'done', failed: 'failed' }; const statusMap = { waiting: 'queued', active: 'running', completed: 'done', failed: 'failed' };
@ -87,34 +88,47 @@ function Jobs({ navigate }) {
// stalled-active job (worker died mid-process, holding a concurrency slot) // stalled-active job (worker died mid-process, holding a concurrency slot)
// gets yanked and the next queued job runs. mode just changes the prompt // gets yanked and the next queued job runs. mode just changes the prompt
// copy so the operator knows what they're doing. // copy so the operator knows what they're doing.
const handleDelete = React.useCallback((job, mode) => { const handleDelete = React.useCallback(async (job, mode) => {
const msg = mode === 'cancel' const msg = mode === 'cancel'
? 'Cancel this running ' + job.kind + ' job?\n\nThe worker may run a few seconds longer in the background, but its result will be discarded and the queue slot frees up immediately.' ? 'Cancel this running ' + job.kind + ' job?\n\nThe worker may run a few seconds longer in the background, but its result will be discarded and the queue slot frees up immediately.'
: 'Remove this ' + job.status + ' ' + job.kind + ' job from the queue?'; : 'Remove this ' + job.status + ' ' + job.kind + ' job from the queue?';
if (!window.confirm(msg)) return; if (!(await confirm({
title: mode === 'cancel' ? 'Cancel job?' : 'Remove job?',
message: msg,
confirmLabel: mode === 'cancel' ? 'Cancel job' : 'Remove',
}))) return;
window.ZAMPP_API.fetch('/jobs/' + job.id, { method: 'DELETE' }) window.ZAMPP_API.fetch('/jobs/' + job.id, { method: 'DELETE' })
.then(() => setJobs(prev => prev.filter(j => j.id !== job.id))) .then(() => setJobs(prev => prev.filter(j => j.id !== job.id)))
.catch(e => alert((mode === 'cancel' ? 'Cancel' : 'Delete') + ' failed: ' + e.message)); .catch(e => alert((mode === 'cancel' ? 'Cancel' : 'Delete') + ' failed: ' + e.message));
}, []); }, [confirm]);
// Retry every failed job at once. Useful after a transient infra issue // Retry every failed job at once. Useful after a transient infra issue
// (S3 outage, hung worker) - one click per job is painful with 20+ failures. // (S3 outage, hung worker) - one click per job is painful with 20+ failures.
const handleRetryAll = React.useCallback(() => { const handleRetryAll = React.useCallback(async () => {
const failedJobs = jobs.filter(j => j.status === 'failed'); const failedJobs = jobs.filter(j => j.status === 'failed');
if (failedJobs.length === 0) return; if (failedJobs.length === 0) return;
if (!window.confirm(`Re-queue all ${failedJobs.length} failed jobs?`)) return; if (!(await confirm({
title: 'Re-queue failed jobs?',
message: `Re-queue all ${failedJobs.length} failed jobs?`,
confirmLabel: 'Re-queue',
danger: false,
}))) return;
Promise.allSettled( Promise.allSettled(
failedJobs.map(j => window.ZAMPP_API.fetch('/jobs/' + j.id + '/retry', { method: 'POST' })) failedJobs.map(j => window.ZAMPP_API.fetch('/jobs/' + j.id + '/retry', { method: 'POST' }))
).then(refresh); ).then(refresh);
}, [jobs, refresh]); }, [jobs, refresh, confirm]);
// Drop every failed job from the queue. The opposite of Retry all used // Drop every failed job from the queue. The opposite of Retry all used
// when a batch of jobs is unrecoverable (e.g. assets that were deleted // when a batch of jobs is unrecoverable (e.g. assets that were deleted
// mid-encode) and the operator just wants the queue cleared. // mid-encode) and the operator just wants the queue cleared.
const handleCancelAll = React.useCallback(() => { const handleCancelAll = React.useCallback(async () => {
const failedJobs = jobs.filter(j => j.status === 'failed'); const failedJobs = jobs.filter(j => j.status === 'failed');
if (failedJobs.length === 0) return; if (failedJobs.length === 0) return;
if (!window.confirm(`Remove all ${failedJobs.length} failed jobs from the queue?\nThis cannot be undone.`)) return; if (!(await confirm({
title: 'Remove all failed jobs?',
message: `Remove all ${failedJobs.length} failed jobs from the queue?\nThis cannot be undone.`,
confirmLabel: 'Remove all',
}))) return;
Promise.allSettled( Promise.allSettled(
failedJobs.map(j => window.ZAMPP_API.fetch('/jobs/' + j.id, { method: 'DELETE' })) failedJobs.map(j => window.ZAMPP_API.fetch('/jobs/' + j.id, { method: 'DELETE' }))
).then(() => { ).then(() => {
@ -123,7 +137,7 @@ function Jobs({ navigate }) {
setJobs(prev => prev.filter(j => j.status !== 'failed')); setJobs(prev => prev.filter(j => j.status !== 'failed'));
refresh(); refresh();
}); });
}, [jobs, refresh]); }, [jobs, refresh, confirm]);
const counts = { const counts = {
all: jobs.length, all: jobs.length,
@ -136,6 +150,7 @@ function Jobs({ navigate }) {
return ( return (
<div className="page"> <div className="page">
{confirmModal}
<div className="page-header"> <div className="page-header">
<h1>Jobs</h1> <h1>Jobs</h1>
<span className="subtitle">Proxy generation, transcoding, and processing queue</span> <span className="subtitle">Proxy generation, transcoding, and processing queue</span>

View file

@ -50,6 +50,7 @@ function Library({ navigate, onOpenAsset, openProject, onClearProject }) {
const [allAssets, setAllAssets] = React.useState(window.ZAMPP_DATA.ASSETS || []); const [allAssets, setAllAssets] = React.useState(window.ZAMPP_DATA.ASSETS || []);
const [ctxMenu, setCtxMenu] = React.useState(null); // { asset, x, y } const [ctxMenu, setCtxMenu] = React.useState(null); // { asset, x, y }
const [renamingAsset, setRenamingAsset] = React.useState(null); const [renamingAsset, setRenamingAsset] = React.useState(null);
const [confirm, confirmModal] = window.useConfirm();
// Asset queued for hi-res download. Null means no modal showing. Set when // Asset queued for hi-res download. Null means no modal showing. Set when
// the user clicks Download and has NOT dismissed the "are you sure" warning. // the user clicks Download and has NOT dismissed the "are you sure" warning.
const [pendingDownload, setPendingDownload] = React.useState(null); const [pendingDownload, setPendingDownload] = React.useState(null);
@ -76,6 +77,9 @@ function Library({ navigate, onOpenAsset, openProject, onClearProject }) {
// Rename project state // Rename project state
const [renamingProject, setRenamingProject] = React.useState(null); const [renamingProject, setRenamingProject] = React.useState(null);
const [projVersion, setProjVersion] = React.useState(0); const [projVersion, setProjVersion] = React.useState(0);
// Multi-select state
const [selectedAssets, setSelectedAssets] = React.useState(new Set());
const [selectionMode, setSelectionMode] = React.useState(false);
const refreshAssets = React.useCallback(() => { const refreshAssets = React.useCallback(() => {
window.ZAMPP_API.refreshAssets() window.ZAMPP_API.refreshAssets()
@ -85,6 +89,76 @@ function Library({ navigate, onOpenAsset, openProject, onClearProject }) {
.catch(() => {}); .catch(() => {});
}, []); }, []);
const toggleSelection = function(assetId, e) {
if (e) e.stopPropagation();
setSelectedAssets(function(prev) {
var next = new Set(prev);
if (next.has(assetId)) next.delete(assetId);
else next.add(assetId);
return next;
});
};
const selectAll = function() {
setSelectedAssets(new Set(assets.map(function(a) { return a.id; })));
};
const clearSelection = function() {
setSelectedAssets(new Set());
setSelectionMode(false);
};
const bulkMoveToBin = async function(binId) {
var ids = Array.from(selectedAssets);
if (ids.length === 0) return;
var targetBin = bins.find(function(b) { return b.id === binId; });
for (var i = 0; i < ids.length; i++) {
var asset = allAssets.find(function(a) { return a.id === ids[i]; });
if (asset && targetBin && asset.project_id && targetBin.project_id && asset.project_id !== targetBin.project_id) {
alert('Cannot move assets to a bin in a different project.');
return;
}
}
try {
await Promise.all(ids.map(function(id) {
return window.ZAMPP_API.fetch('/assets/' + id, { method: 'PATCH', body: JSON.stringify({ bin_id: binId }) });
}));
refreshAssets();
window.dispatchEvent(new Event('df:bins-changed'));
clearSelection();
} catch (e) {
alert('Bulk move failed: ' + e.message);
}
};
const bulkDelete = async function() {
var ids = Array.from(selectedAssets);
if (ids.length === 0) return;
if (!(await confirm({
title: 'Delete ' + ids.length + ' assets?',
message: 'Delete ' + ids.length + ' assets permanently?\nThis removes the database rows and S3 objects.\nThis cannot be undone.',
}))) return;
try {
await Promise.all(ids.map(function(id) {
return window.ZAMPP_API.fetch('/assets/' + id + '?hard=true', { method: 'DELETE' });
}));
refreshAssets();
clearSelection();
} catch (e) {
alert('Bulk delete failed: ' + e.message);
}
};
const deleteAsset = React.useCallback(async (asset) => {
if (!(await confirm({
title: 'Delete asset?',
message: 'Delete "' + (asset.display_name || asset.name) + '" permanently?\nThis removes the database row and the S3 objects.\nThis cannot be undone.',
}))) return;
window.ZAMPP_API.fetch('/assets/' + asset.id + '?hard=true', { method: 'DELETE' })
.then(refreshAssets)
.catch(function(e) { alert('Delete failed: ' + e.message); });
}, [confirm, refreshAssets]);
// Auto-refresh: poll the library while it's open so live recordings flip // Auto-refresh: poll the library while it's open so live recordings flip
// to 'ready' (with thumbnail) without a manual reload. Also pull once on // to 'ready' (with thumbnail) without a manual reload. Also pull once on
// mount so uploads/imports created on other screens appear immediately. // mount so uploads/imports created on other screens appear immediately.
@ -222,6 +296,7 @@ function Library({ navigate, onOpenAsset, openProject, onClearProject }) {
return ( return (
<div className="library-layout"> <div className="library-layout">
{confirmModal}
<aside className="library-rail"> <aside className="library-rail">
<div> <div>
<h4>Projects</h4> <h4>Projects</h4>
@ -310,6 +385,29 @@ function Library({ navigate, onOpenAsset, openProject, onClearProject }) {
<h1 className="toolbar-title">{displayTitle}</h1> <h1 className="toolbar-title">{displayTitle}</h1>
<span className="count">· {assets.length} assets</span> <span className="count">· {assets.length} assets</span>
<div style={{ flex: 1 }} /> <div style={{ flex: 1 }} />
{selectionMode && selectedAssets.size > 0 && (
<div style={{ display: 'flex', gap: 8, alignItems: 'center', marginRight: 12 }}>
<span style={{ fontSize: 13, color: 'var(--text-2)' }}>{selectedAssets.size} selected</span>
<div className="tab-group">
<button onClick={selectAll} title="Select all"><Icon name="check" size={12} /></button>
<button onClick={clearSelection} title="Clear selection"><Icon name="x" size={12} /></button>
</div>
{BINS.length > 0 && (
<select className="field-input" style={{ height: 32, fontSize: 12, padding: '0 8px' }}
onChange={function(e) { if (e.target.value) bulkMoveToBin(e.target.value); e.target.value = ''; }}
value="">
<option value="">Move to bin</option>
{BINS.filter(function(b) { return !openProject || b.project_id === openProject.id; }).map(function(b) {
return <option key={b.id} value={b.id}>{b.name}</option>;
})}
</select>
)}
<button className="btn danger sm" onClick={bulkDelete}><Icon name="trash" size={12} />Delete</button>
</div>
)}
<button className={'btn ghost sm' + (selectionMode ? ' active' : '')} onClick={function() { setSelectionMode(!selectionMode); if (selectionMode) clearSelection(); }} title="Select multiple">
<Icon name="check" size={12} />Select
</button>
<div className="search" style={{ width: 220 }}> <div className="search" style={{ width: 220 }}>
<Icon name="search" className="search-icon" /> <Icon name="search" className="search-icon" />
<input value={search} onChange={function(e) { setSearch(e.target.value); }} placeholder="Filter assets…" /> <input value={search} onChange={function(e) { setSearch(e.target.value); }} placeholder="Filter assets…" />
@ -340,18 +438,27 @@ function Library({ navigate, onOpenAsset, openProject, onClearProject }) {
onContextMenu={function(e) { openCtx(a, e); }} onContextMenu={function(e) { openCtx(a, e); }}
onDownload={function() { requestDownload(a); }} onDownload={function() { requestDownload(a); }}
onDragStart={function(e) { onAssetDragStart(a.id, e); }} onDragStart={function(e) { onAssetDragStart(a.id, e); }}
draggable={true} />; draggable={true}
selectionMode={selectionMode}
isSelected={selectedAssets.has(a.id)}
onToggleSelect={function(e) { toggleSelection(a.id, e); }} />;
})} })}
</div> </div>
) : ( ) : (
<div className="library-list"> <div className="library-list">
<div className="list-row head"> <div className="list-row head">
{selectionMode && <div style={{ width: 32 }}></div>}
<div></div><div>Name</div><div>Duration</div><div>Resolution</div><div>Codec</div><div>Size</div><div>Updated</div><div></div> <div></div><div>Name</div><div>Duration</div><div>Resolution</div><div>Codec</div><div>Size</div><div>Updated</div><div></div>
</div> </div>
{assets.map(function(a) { {assets.map(function(a) {
return ( return (
<div key={a.id} className="list-row" onClick={function() { onOpenAsset(a); }} onContextMenu={function(e) { openCtx(a, e); }} style={{ cursor: 'pointer' }} <div key={a.id} className="list-row" onClick={function() { if (!selectionMode) onOpenAsset(a); }} onContextMenu={function(e) { openCtx(a, e); }} style={{ cursor: 'pointer' }}
draggable="true" onDragStart={function(e) { onAssetDragStart(a.id, e); }}> draggable="true" onDragStart={function(e) { onAssetDragStart(a.id, e); }}>
{selectionMode && (
<div style={{ width: 32, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<input type="checkbox" checked={selectedAssets.has(a.id)} onChange={function(e) { toggleSelection(a.id, e); }} onClick={function(e) { e.stopPropagation(); }} style={{ cursor: 'pointer' }} />
</div>
)}
<div className="thumb"><AssetThumb asset={a} /></div> <div className="thumb"><AssetThumb asset={a} /></div>
<div> <div>
<div className="name">{a.name}</div> <div className="name">{a.name}</div>
@ -383,6 +490,7 @@ function Library({ navigate, onOpenAsset, openProject, onClearProject }) {
onOpen={function() { onOpenAsset(ctxMenu.asset); }} onOpen={function() { onOpenAsset(ctxMenu.asset); }}
onRename={function(a) { setCtxMenu(null); setRenamingAsset(a); }} onRename={function(a) { setCtxMenu(null); setRenamingAsset(a); }}
onDownload={function(a) { setCtxMenu(null); requestDownload(a); }} onDownload={function(a) { setCtxMenu(null); requestDownload(a); }}
onDelete={function(a) { setCtxMenu(null); deleteAsset(a); }}
/> />
)} )}
{pendingDownload && ( {pendingDownload && (
@ -466,7 +574,7 @@ function runDownload(asset) {
.catch(function(e) { alert('Download failed: ' + (e.message || 'unknown error')); }); .catch(function(e) { alert('Download failed: ' + (e.message || 'unknown error')); });
} }
function AssetContextMenu({ asset, x, y, bins, onClose, onChanged, onOpen, onRename, onDownload }) { function AssetContextMenu({ asset, x, y, bins, onClose, onChanged, onOpen, onRename, onDownload, onDelete }) {
const ref = React.useRef(null); const ref = React.useRef(null);
// Pin the menu inside the viewport even if the user right-clicked near // Pin the menu inside the viewport even if the user right-clicked near
// the bottom-right edge of the grid. // the bottom-right edge of the grid.
@ -496,11 +604,8 @@ function AssetContextMenu({ asset, x, y, bins, onClose, onChanged, onOpen, onRen
}; };
const remove = function() { const remove = function() {
if (onDelete) { onDelete(asset); return; }
onClose(); onClose();
if (!confirm('Delete "' + (asset.display_name || asset.name) + '" permanently?\nThis removes the database row and the S3 objects.\nThis cannot be undone.')) return;
window.ZAMPP_API.fetch('/assets/' + asset.id + '?hard=true', { method: 'DELETE' })
.then(onChanged)
.catch(function(e) { alert('Delete failed: ' + e.message); });
}; };
return ( return (
@ -545,7 +650,7 @@ function AssetContextMenu({ asset, x, y, bins, onClose, onChanged, onOpen, onRen
); );
} }
function AssetCard({ asset, onOpen, onContextMenu, onDownload, onDragStart, draggable }) { function AssetCard({ asset, onOpen, onContextMenu, onDownload, onDragStart, draggable, selectionMode, isSelected, onToggleSelect }) {
const [hoverStream, setHoverStream] = React.useState(null); const [hoverStream, setHoverStream] = React.useState(null);
const [hovered, setHovered] = React.useState(false); const [hovered, setHovered] = React.useState(false);
const timerRef = React.useRef(null); const timerRef = React.useRef(null);
@ -583,9 +688,14 @@ function AssetCard({ asset, onOpen, onContextMenu, onDownload, onDragStart, drag
const showVideo = hovered && hoverStream; const showVideo = hovered && hoverStream;
return ( return (
<div className="asset-card" onClick={onOpen} onContextMenu={onContextMenu} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} <div className="asset-card" onClick={selectionMode ? onToggleSelect : onOpen} onContextMenu={onContextMenu} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}
draggable={draggable} onDragStart={onDragStart}> draggable={draggable} onDragStart={onDragStart}>
<div style={{ position: 'relative' }}> <div style={{ position: 'relative' }}>
{selectionMode && (
<div style={{ position: 'absolute', top: 8, left: 8, zIndex: 10 }}>
<input type="checkbox" checked={isSelected} onChange={onToggleSelect} onClick={function(e) { e.stopPropagation(); }} style={{ cursor: 'pointer', width: 18, height: 18 }} />
</div>
)}
<AssetThumb asset={asset} /> <AssetThumb asset={asset} />
{showVideo && ( {showVideo && (
<video <video

File diff suppressed because it is too large Load diff

Some files were not shown because too many files have changed in this diff Show more