HLS VOD playback for browser (supplements MP4 proxy) #170

Open
zgaetano wants to merge 7 commits from feat/hls-vod-playback into main
125 changed files with 14229 additions and 1109 deletions
Showing only changes of commit 39e010544c - Show all commits

1
.claude/launch.json Normal file
View file

@ -0,0 +1 @@
{"version":"0.0.1","configurations":[{"name":"web-ui","runtimeExecutable":"npx","runtimeArgs":["serve","services/web-ui/public","--listen","47434","--no-clipboard"],"port":47434}]}

View file

@ -22,6 +22,11 @@ SESSION_SECRET=changeme
# MAM API Configuration
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
# 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.

4
.gitignore vendored
View file

@ -27,8 +27,8 @@ services/editor/**/node_modules
services/editor/**/dist
services/editor/.pnpm-store
# Blackmagic DeckLink SDK + runtime libs (operator-supplied; see services/capture/build-with-decklink.sh)
services/capture/sdk/
# Blackmagic DeckLink SDK headers are now committed (private/internal repo) under services/capture/sdk/.
# Runtime .so libs (libDeckLinkAPI.so) come from the DesktopVideo driver install and are not committed.
services/capture/lib/
# 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:
# MAM_API_URL REQUIRED Primary MAM API base URL
# 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)
# AGENT_PORT Host port for the node agent (default: 7436)
# 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")
# 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}"
MAM_API_URL="${MAM_API_URL:-}"
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_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}"
PROFILES="${PROFILES:-}"
BMD_MODEL="${BMD_MODEL:-}"
@ -65,6 +74,37 @@ detect_lan_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 ────────────────────────────────────────────────────────────────
echo -e "\n${BLD}${CYN}Wild Dragon MAM — Cluster Node Onboarding${NC}\n"
@ -79,6 +119,36 @@ if [[ -z "$NODE_IP" ]]; then
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 "Role : $NODE_ROLE"
info "Agent port : $AGENT_PORT"
@ -135,6 +205,7 @@ info "Writing $ENV_FILE"
echo "MAM_API_URL=$MAM_API_URL"
echo "NODE_TOKEN=$NODE_TOKEN"
echo "NODE_ROLE=$NODE_ROLE"
echo "NODE_NAME=$NODE_NAME"
echo "NODE_IP=$NODE_IP"
echo "AGENT_PORT=$AGENT_PORT"
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

@ -16,6 +16,11 @@
# - Sets NVENC_ENABLED=true so the worker prioritises h264_nvenc/hevc_nvenc
services:
capture:
runtime: nvidia
environment:
NVIDIA_VISIBLE_DEVICES: all
NVIDIA_DRIVER_CAPABILITIES: video,compute,utility
worker:
build:
context: ./services/worker

View file

@ -43,10 +43,15 @@ services:
build: ./services/node-agent
restart: unless-stopped
network_mode: host
pid: host
environment:
MAM_API_URL: ${MAM_API_URL}
NODE_TOKEN: ${NODE_TOKEN:-}
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:-}
AGENT_PORT: ${AGENT_PORT:-7436}
HEARTBEAT_MS: ${HEARTBEAT_MS:-30000}
@ -55,10 +60,25 @@ services:
BMD_MODEL: ${BMD_MODEL:-}
BMD_DEVICE_PREFIX: ${BMD_DEVICE_PREFIX:-dv}
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:
- /var/run/docker.sock:/var/run/docker.sock
- /dev:/dev: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:
- /dev/blackmagic:/dev/blackmagic
@ -66,6 +86,7 @@ services:
build: ./services/worker
profiles: [worker]
restart: unless-stopped
privileged: true
environment:
REDIS_URL: ${REDIS_URL}
DATABASE_URL: ${DATABASE_URL}
@ -75,6 +96,7 @@ services:
S3_SECRET_KEY: ${S3_SECRET_KEY}
S3_REGION: ${S3_REGION:-us-east-1}
NVENC_ENABLED: ${NVENC_ENABLED:-false}
GROWING_PATH: /growing
networks:
- wild-dragon-worker
@ -84,6 +106,7 @@ services:
build: ./services/capture
profiles: [capture]
restart: unless-stopped
runtime: nvidia
environment:
REDIS_URL: ${REDIS_URL}
DATABASE_URL: ${DATABASE_URL}
@ -92,6 +115,8 @@ services:
S3_ACCESS_KEY: ${S3_ACCESS_KEY}
S3_SECRET_KEY: ${S3_SECRET_KEY}
CAPTURE_PORT: 3001
NVIDIA_VISIBLE_DEVICES: all
NVIDIA_DRIVER_CAPABILITIES: video,compute,utility
devices:
- ${BMD_DEVICE_0:-/dev/blackmagic/dv0}:/dev/blackmagic/dv0
- ${BMD_DEVICE_1:-/dev/blackmagic/dv1}:/dev/blackmagic/dv1

View file

@ -61,6 +61,11 @@ services:
DOCKER_NETWORK: wild-dragon_wild-dragon
NODE_IP: ${NODE_IP}
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}
PLAYOUT_IMAGE: ${PLAYOUT_IMAGE:-wild-dragon-playout:latest}
PLAYOUT_AMCP_BASE_PORT: ${PLAYOUT_AMCP_BASE_PORT:-5250}
@ -107,6 +112,11 @@ services:
dockerfile: Dockerfile.gpu
image: wild-dragon-worker-gpu:latest
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:
- queue
- db
@ -123,7 +133,7 @@ services:
# after the capability-routing split, so import jobs sat unprocessed and
# assets stayed `ingesting` forever. import is concurrency-1 + network-
# bound, so one consumer (this heavy/primary worker) is sufficient.
WORKER_QUEUES: proxy,conform,trim,import,playout-stage
WORKER_QUEUES: proxy,conform,trim,import,playout-stage,promotion
RUN_PROMOTION: "true"
PROXY_CONCURRENCY: "2"
PLAYOUT_MEDIA_DIR: /media
@ -131,7 +141,9 @@ services:
WORKER_LABEL: "zampp1 / Tesla P4"
NVIDIA_DRIVER_CAPABILITIES: video,compute,utility
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
networks:
- 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

@ -0,0 +1,7 @@
# Build artifacts that must never enter the Docker build context — a stale
# CMakeCache.txt from a native bridge build breaks the in-image cmake step
# ("CMakeCache.txt directory is different / source does not match").
deltacast-bridge/build/
node_modules/
*.bak
*.log

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
# 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)
@ -64,6 +81,34 @@ RUN ./configure \
RUN /usr/local/bin/ffmpeg -hide_banner -encoders 2>&1 | grep -E 'nvenc' \
|| (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 ───────────────────────────────────────────────────
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 \
libsrt1.5-openssl libzmq5 libstdc++6 libc++1 libc++abi1 \
cifs-utils util-linux \
libexpat1 liburiparser1 libuuid1 \
&& rm -rf /var/lib/apt/lists/*
# Copy compiled ffmpeg/ffprobe
@ -85,7 +131,34 @@ COPY --from=ffmpeg-builder /usr/local/lib/ /usr/local/lib/
# DeckLink runtime .so
COPY lib/libDeckLinkAPI.so /usr/lib/libDeckLinkAPI.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.
# /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,693 @@
/* 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"
#ifndef F_SETPIPE_SZ
#define F_SETPIPE_SZ 1031
#endif
/* ── Constants ────────────────────────────────────────────────────────── */
#define MAX_PORTS 8
/* ── Globals ──────────────────────────────────────────────────────────── */
static atomic_int g_stop = 0; /* global shutdown (SIGTERM/SIGINT only) */
static void on_signal(int s) { (void)s; atomic_store(&g_stop, 1); }
/* Per-port stop flag — set only when a fatal error occurs on that specific
* port (e.g. video lock lost). Audio EPIPE is handled by reopening the FIFO
* rather than stopping the port, so the thread survives ffmpeg restarts. */
static atomic_int g_port_stop[MAX_PORTS];
/* ── 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 -1; /* ports 4-7 have no passive loopback property; call site guards p < 4 */
}
}
/* ── 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 ─────────────────────────────────────────────────── */
/* Writes all bytes to fd. Uses non-blocking I/O so the bridge never stalls
* waiting for a slow reader. Returns 0 on success, -1 on fatal error (EPIPE
* = reader closed the FIFO). EAGAIN / EWOULDBLOCK on a full pipe is NOT fatal
* the caller (video_thread) will retry on the next slot lock. */
static int write_all(int fd, const unsigned char *p, size_t len) {
/* Make the fd non-blocking for the duration of this write */
int flags = fcntl(fd, F_GETFL, 0);
if (flags < 0) return -1;
if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) return -1;
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;
if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
/* Pipe full — brief yield then retry */
struct timespec ts = {0, 1000000L}; /* 1ms */
nanosleep(&ts, NULL);
continue;
}
/* EPIPE or other fatal error — restore flags and return */
fcntl(fd, F_SETFL, flags);
return -1;
}
/* Restore blocking mode */
fcntl(fd, F_SETFL, flags);
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 ──────────────────────────────────────────────────────
*
* - Opens FIFO writer (blocks until a reader connects correct behaviour).
* - Feeds continuous wall-clock-paced s16le stereo (real or silence).
* - Best-effort VHD audio stream; silence fallback on any failure.
* - On EPIPE (ffmpeg reader died): closes and REOPENS the FIFO so the
* thread survives an ffmpeg restart without bringing down other ports.
* EPIPE never sets g_stop only SIGTERM/SIGINT does that.
*/
static void *audio_thread(void *arg) {
PortState *ps = (PortState *)arg;
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) return NULL;
/* Open the VHD audio stream once for the lifetime of the bridge.
* The stream stays open across reader reconnects no need to reopen it. */
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) {
/* Per Deltacast SDK Sample_RXAudio.cpp: VHD_SDI_SP_INTERFACE must be
* propagated to the audio stream, otherwise VHD_SlotExtractAudio
* returns 0 samples (silent capture). */
ULONG iface = 0;
VHD_GetStreamProperty(stream, VHD_SDI_SP_INTERFACE, &iface);
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);
VHD_SetStreamProperty(stream, VHD_SDI_SP_INTERFACE, iface);
/* Configure BOTH channels of the stereo pair (group 0). The actual PCM
* samples land in pAudioChannels[0].pData (packed L/R s16le). Channel
* [1] must declare Mode+BufferFormat so the SDK recognizes the pair. */
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;
ai.pAudioGroups[0].pAudioChannels[1].Mode = VHD_AM_STEREO;
ai.pAudioGroups[0].pAudioChannels[1].BufferFormat = VHD_AF_16;
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);
}
long frame_ns = (long)(1000000000.0 * (double)fps_den / (double)fps_num);
HANDLE slot = NULL;
/* Outer loop: reopen the FIFO writer each time a reader connects.
* This allows the bridge to survive ffmpeg session stop/restart on a port
* without affecting any other port's threads. */
while (!atomic_load(&g_stop) && !atomic_load(&g_port_stop[ps->port])) {
int fd = open(ps->audio_fifo, O_WRONLY);
if (fd < 0) {
/* Open failed (rare — FIFO was deleted?). Brief pause then retry. */
fprintf(stderr, "[audio:%u] open FIFO failed: %s\n", ps->port, strerror(errno));
struct timespec ts = {0, 200000000L};
nanosleep(&ts, NULL);
continue;
}
fcntl(fd, F_SETPIPE_SZ, 1024 * 1024);
/* Reset wall-clock baseline after potentially blocking on open().
* Only used for the SILENCE fallback path (no hardware audio). */
struct timespec next;
clock_gettime(CLOCK_MONOTONIC, &next);
/* Inner loop: feed audio into the open FIFO until reader exits (EPIPE). */
while (!atomic_load(&g_stop) && !atomic_load(&g_port_stop[ps->port])) {
size_t out_bytes = 0;
if (have_vhd_audio) {
/* HARDWARE-PACED PATH (the normal case).
* VHD_LockSlotHandle blocks until the board has the next audio
* slot ready this slot is generated from the SAME SDI signal
* as the video, so blocking here paces audio in lockstep with
* video at the TRUE hardware rate. We write ONLY the real
* samples the board gives us (no silence padding, no wall-clock
* sleep) so the audio timeline length exactly tracks video.
* This is the fix for progressive A/V drift: mixing wall-clock
* paced silence with variable-length real reads made the audio
* stream length diverge from the video stream length. */
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);
if (out_bytes > 0) {
if (write_all(fd, buf, out_bytes) < 0) {
fprintf(stderr, "[audio:%u] EPIPE — waiting for next reader\n", ps->port);
break;
}
}
/* No wall-clock sleep — the board's slot cadence is the clock. */
continue;
} else if (r == VHDERR_TIMEOUT) {
/* No slot yet — loop and try again (do NOT inject silence,
* that would add extra samples and cause drift). */
continue;
} else {
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;
clock_gettime(CLOCK_MONOTONIC, &next); /* rebase silence clock */
}
}
/* SILENCE FALLBACK PATH (no hardware audio available).
* Wall-clock paced one-frame-of-silence per video-frame interval so
* ffmpeg's input 1 never starves and audio length still tracks
* real time. */
memset(buf, 0, tick_bytes);
out_bytes = tick_bytes;
if (write_all(fd, buf, out_bytes) < 0) {
fprintf(stderr, "[audio:%u] EPIPE — waiting for next reader\n", ps->port);
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;
/* Outer loop: reopen the FIFO writer each time a reader connects.
* Mirror the audio thread pattern EPIPE means the ffmpeg sidecar for
* this port died (session stop/restart), NOT a hardware fault. We reopen
* and block until the next recorder start; other ports are unaffected. */
while (!atomic_load(&g_stop) && !atomic_load(&g_port_stop[ps->port])) {
int fd = open(ps->video_fifo, O_WRONLY);
if (fd < 0) {
fprintf(stderr, "[video:%u] open FIFO failed: %s\n", ps->port, strerror(errno));
struct timespec ts = {0, 200000000L};
nanosleep(&ts, NULL);
continue;
}
{
int pipe_sz = 64 * 1024 * 1024; /* 64 MB — ~16 frames of 1080p UYVY */
if (fcntl(fd, F_SETPIPE_SZ, pipe_sz) < 0) {
fprintf(stderr, "[video:%u] fcntl F_SETPIPE_SZ failed: %s\n", ps->port, strerror(errno));
}
}
HANDLE slot = NULL;
int fatal = 0;
while (!atomic_load(&g_stop) && !atomic_load(&g_port_stop[ps->port])) {
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) {
ULONG expected = (ULONG)ps->vi.width * (ULONG)ps->vi.height * 2;
if (sz != expected) {
fprintf(stderr, "[video:%u] WARN: slot sz=%lu != expected %lu (w=%d h=%d) -- packing mismatch; skipping frame\n",
ps->port, sz, expected, ps->vi.width, ps->vi.height);
VHD_UnlockSlotHandle(slot);
continue;
}
if (write_all(fd, buf, sz) < 0) {
/* EPIPE: sidecar died (session stop/restart).
* Break to outer loop reopen for next session. */
fprintf(stderr, "[video:%u] EPIPE — waiting for next reader\n", ps->port);
VHD_UnlockSlotHandle(slot);
break;
}
}
VHD_UnlockSlotHandle(slot);
} else if (r != VHDERR_TIMEOUT) {
fprintf(stderr, "[video:%u] VHD_LockSlotHandle error %lu — stopping port\n",
ps->port, r);
atomic_store(&g_port_stop[ps->port], 1);
fatal = 1;
break;
}
}
close(fd);
if (fatal) break;
}
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;
}
/* ── Configure bi-directional channel mode before opening board ─────
*
* The DELTA-12G-e-h 8c is a bidirectional card. Unless we explicitly
* call VHD_SetBiDirCfg(BrdId, VHD_BIDIR_80) the board may default to
* a mixed RX/TX configuration (e.g. 4RX/4TX), which causes random RX
* stream opens to fail with VHDERR_RESOURCEUNAVAILABLE and produces the
* "connecting…" hang operators see when starting certain recorders.
*
* Per SDK sample Tools.cpp SetNbChannels(): open a temporary handle,
* check IS_BIDIR and channel counts, call VHD_SetBiDirCfg if needed,
* then close. The subsequent real board open will see all 8 as RX.
*/
{
HANDLE tmp = NULL;
if (VHD_OpenBoardHandle(device_id, &tmp, NULL, 0) == VHDERR_NOERROR) {
ULONG nb_rx = 0, nb_tx = 0, is_bidir = 0;
VHD_GetBoardProperty(tmp, VHD_CORE_BP_NB_RXCHANNELS, &nb_rx);
VHD_GetBoardProperty(tmp, VHD_CORE_BP_NB_TXCHANNELS, &nb_tx);
VHD_GetBoardProperty(tmp, VHD_CORE_BP_IS_BIDIR, &is_bidir);
VHD_CloseBoardHandle(tmp);
if (is_bidir) {
/* Set all channels to RX. For 8-channel bidir: VHD_BIDIR_80.
* VHD_SetBiDirCfg takes the board INDEX, not a handle. */
ULONG cfg = (nb_rx + nb_tx == 8) ? VHD_BIDIR_80 : VHD_BIDIR_40;
ULONG r = VHD_SetBiDirCfg(device_id, cfg);
if (r == VHDERR_NOERROR)
fprintf(stderr, "[board] SetBiDirCfg(%lu) OK — %lu+%lu ch bidir configured all-RX\n",
cfg, nb_rx, nb_tx);
else
fprintf(stderr, "[board] SetBiDirCfg warn rc=%lu (non-fatal)\n", r);
}
}
}
/* ── 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);
/* Per SDK samples: for 12G-ASI or 3G-ASI channel types the channel must be
* explicitly switched to SDI mode. Without this, VHD_SDI_CP_VIDEO_STANDARD
* polls return NB_VHD_VIDEOSTANDARDS (no signal) even when signal present.
* Also disable passive loopback for ports 0-3 so RX doesn't loop to TX. */
for (int pi = 0; pi < port_count; pi++) {
unsigned p = ports[pi];
ULONG chn_type = 0;
if (VHD_GetChannelProperty(board, VHD_RX_CHANNEL, p, VHD_CORE_CP_TYPE, &chn_type) == VHDERR_NOERROR) {
if (chn_type == VHD_CHNTYPE_3GSDI_ASI || chn_type == VHD_CHNTYPE_12GSDI_ASI)
VHD_SetChannelProperty(board, VHD_RX_CHANNEL, p, VHD_CORE_CP_MODE, VHD_CHANNEL_MODE_SDI);
}
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;
/* Initialise per-port stop flags. */
for (int pi = 0; pi < MAX_PORTS; pi++) atomic_store(&g_port_stop[pi], 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);
ULONG iface = 0;
if (VHD_GetStreamProperty(vs, VHD_SDI_SP_INTERFACE, &iface) == VHDERR_NOERROR) {
VHD_SetStreamProperty(vs, VHD_SDI_SP_INTERFACE, iface);
fprintf(stderr, "[board] port %u explicitly set SDI Interface to %lu\n", ports[pi], iface);
}
/* Pin to tightly-packed 8-bit UYVY. Relying on SDK default caused
* the board to deliver frames whose size != width*height*2,
* producing rolled/sheared ("bouncing and bending") video. */
VHD_SetStreamProperty(vs, VHD_CORE_SP_BUFFER_PACKING, VHD_BUFPACK_VIDEO_YUV422_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

@ -23,6 +23,12 @@ app.use('/capture', captureRoutes);
const server = app.listen(PORT, () => {
console.log(`Wild Dragon Capture Service listening on port ${PORT}`);
bootstrapAutoStart();
// Auto-start idle signal preview for deltacast/sdi sidecars.
// 3s delay lets the deltacast bridge FIFOs come up first.
const _srcType = process.env.SOURCE_TYPE;
if (process.env.RECORDER_ID && (_srcType === 'deltacast' || _srcType === 'sdi')) {
setTimeout(() => captureManager.startIdlePreview(), 3000);
}
});
// Mapped from the env vars routes/recorders.js writes into the container.
@ -63,6 +69,13 @@ async function bootstrapAutoStart() {
const streamKey = envOpt('STREAM_KEY');
const sourceUrl = envOpt('SOURCE_URL');
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'})...`);
try {
@ -72,6 +85,8 @@ async function bootstrapAutoStart() {
binId: envOpt('BIN_ID') || null,
clipName,
device,
port,
board,
sourceType,
sourceUrl,
listen,
@ -135,6 +150,25 @@ async function gracefulShutdown(signal) {
console.error('[shutdown] failed to flag empty asset:', e.message);
}
}
} else if (completed.growingPath) {
// Growing-files recorder: the master lives on the SMB share. Mark the asset
// as pending_migration so the UI shows it is on SMB and provides a manual
// right-click option to promote it to S3.
console.log(`[shutdown] growing capture finalized on share (${completed.growingPath}); flagging pending_migration`);
try {
const res = await fetch(`${MAM_API_URL}/api/v1/assets/${liveAssetId}/pending-migration`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...(MAM_API_TOKEN ? { 'Authorization': `Bearer ${MAM_API_TOKEN}` } : {}) },
body: JSON.stringify({ duration: completed.duration }),
});
if (!res.ok) {
console.warn(`[shutdown] mam-api pending-migration returned ${res.status}: ${await res.text()}`);
} else {
console.log('[shutdown] live asset flagged pending_migration with mam-api');
}
} catch (mamErr) {
console.error('[shutdown] failed to flag pending_migration:', mamErr.message);
}
} else if (liveAssetId) {
// Finalise the pre-created live asset by id (avoids POST / 409 collision).
try {

View file

@ -77,6 +77,7 @@ function classifyProbeError(raw, sourceType) {
const router = express.Router();
const MAM_API_URL = process.env.MAM_API_URL || 'http://mam-api:3000';
const MAM_API_TOKEN = process.env.MAM_API_TOKEN || '';
/**
* GET /devices
@ -325,12 +326,43 @@ router.post('/start', async (req, res) => {
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 {
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`,
});
}
// Create live asset in MAM API before starting capture
let assetId;
try {
const mamResponse = await fetch(`${MAM_API_URL}/api/v1/assets`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...(MAM_API_TOKEN ? { Authorization: `Bearer ${MAM_API_TOKEN}` } : { 'X-Requested-With': 'dragonflight-ui' }) },
body: JSON.stringify({
projectId: project_id,
binId: bin_id,
clipName: clip_name,
sourceType: source_type,
status: 'live',
}),
});
if (!mamResponse.ok) {
const errText = await mamResponse.text();
throw new Error(`MAM API returned ${mamResponse.status}: ${errText}`);
}
const asset = await mamResponse.json();
assetId = asset.id;
} catch (mamError) {
console.error('Failed to create live asset:', mamError.message);
return res.status(500).json({ error: `Could not create live asset: ${mamError.message}` });
}
const session = await captureManager.start({
projectId: project_id,
binId: bin_id || null,
@ -341,6 +373,7 @@ router.post('/start', async (req, res) => {
listen,
listenPort: listen_port,
streamKey: stream_key,
assetId,
});
res.json(session);
@ -365,33 +398,34 @@ router.post('/stop', async (req, res) => {
const completedSession = await captureManager.stop(session_id);
// Register asset with mam-api.
// If proxyKey is null (SRT/RTMP source), set needsProxy=true so the
// worker generates a proxy from the hires file asynchronously.
try {
const mamResponse = await fetch(`${MAM_API_URL}/api/v1/assets`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
projectId: completedSession.projectId,
binId: completedSession.binId,
clipName: completedSession.clipName,
sourceType: completedSession.sourceType,
hiresKey: completedSession.hiresKey,
proxyKey: completedSession.proxyKey,
needsProxy: completedSession.proxyKey === null,
duration: completedSession.duration,
capturedAt: completedSession.startedAt,
}),
});
// Finalize the pre-created live asset.
// If it was a growing-file session, we call /pending-migration to flip status
// to 'pending_migration' (on SMB, not S3). Otherwise, we call /finalize to
// kick off the proxy/thumbnail job chain.
if (completedSession.assetId) {
try {
const path = completedSession.growingPath ? 'pending-migration' : 'finalize';
const body = completedSession.growingPath
? { duration: completedSession.duration }
: {
hiresKey: completedSession.hiresKey,
proxyKey: completedSession.proxyKey,
needsProxy: completedSession.proxyKey === null,
duration: completedSession.duration,
capturedAt: completedSession.startedAt,
};
if (!mamResponse.ok) {
console.warn(
`MAM API registration returned ${mamResponse.status}: ${await mamResponse.text()}`,
);
const mamResponse = await fetch(`${MAM_API_URL}/api/v1/assets/${completedSession.assetId}/${path}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (!mamResponse.ok) {
console.warn(`MAM API ${path} returned ${mamResponse.status}: ${await mamResponse.text()}`);
}
} catch (mamError) {
console.warn('Failed to finalize/pending-migrate asset with MAM API:', mamError.message);
}
} catch (mamError) {
console.warn('Failed to register asset with MAM API:', mamError.message);
}
res.json(completedSession);

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

@ -0,0 +1,8 @@
-- 2026-06: add 'pending_migration' to asset_status enum for manual SMB-to-S3 promotion
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_enum WHERE enumlabel = 'pending_migration' AND enumtypid = 'asset_status'::regtype) THEN
ALTER TYPE asset_status ADD VALUE 'pending_migration';
END IF;
END
$$;

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
const pool = process.env.DATABASE_URL

View file

@ -41,18 +41,12 @@ import { startCleanupLoop } from './tasks/cleanupTempSegments.js';
const app = express();
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 || '')
.split(',').map(s => s.trim()).filter(Boolean);
app.use(cors({
origin: (origin, cb) => {
// No Origin header (same-origin or curl) — allow.
if (!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);
return cb(null, false);
},
@ -60,14 +54,8 @@ app.use(cors({
}));
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);
// 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') {
app.use((req, res, next) => {
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) {
console.error('[fatal] SESSION_SECRET is required when AUTH_ENABLED=true');
process.exit(1);
}
// Session — actually wired this time. See specs/2026-05-27-auth-system-design.md.
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,
name: 'dragonflight.sid',
cookie: {
@ -95,36 +79,26 @@ app.use(session({
path: '/',
maxAge: 8 * 3600 * 1000,
},
rolling: false, // sliding renewal handled in requireAuth so idle + absolute can be enforced separately
rolling: false,
resave: false,
saveUninitialized: false,
}));
// ── Health ────────────────────────────────────────────────────────────────────
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([
'/auth/login', '/auth/login/totp', '/auth/setup', '/auth/setup-required',
'/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', (req, res, next) => {
if (UNAUTH_PATHS.has(req.path)) return next();
return requireAuth(req, res, next);
});
// ── API Routes ────────────────────────────────────────────────────────────────
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/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/assets', assetsRouter);
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/storage', storageRouter);
// ── Error handler ─────────────────────────────────────────────────────────────
app.use(errorHandler);
// ── Start ────────────────────────────────────────────────────────────────────
import { readdirSync, readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const __dirnameMig = dirname(fileURLToPath(import.meta.url));
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');
let files = [];
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 allowFailures = process.env.MIGRATIONS_ALLOW_FAILURES === '1';
@ -200,7 +166,6 @@ async function runMigrations() {
console.error('[migration] FAILED ' + f + ': ' + err.message);
client.release();
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.');
process.exit(1);
}
@ -209,13 +174,9 @@ async function runMigrations() {
}
await runMigrations();
// Load S3 config from DB so any settings saved via the Settings page override env vars
await loadS3ConfigFromDb();
// ── Cluster self-heartbeat ────────────────────────────────────────────────────
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;
const ifaces = os.networkInterfaces();
@ -227,9 +188,6 @@ function getLocalIp() {
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() {
return new Promise(resolve => {
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() {
const load = os.loadavg()[0];
const total = os.totalmem();
@ -262,14 +224,15 @@ async function selfHeartbeat() {
pool.query(
`INSERT INTO cluster_nodes
(hostname, ip_address, role, version, api_url,
cpu_usage, mem_used_mb, mem_total_mb, capabilities, last_seen)
VALUES ($1,$2,'primary',$3,$4,$5,$6,$7,$8,NOW())
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(),NOW())
ON CONFLICT (hostname) DO UPDATE SET
ip_address = EXCLUDED.ip_address,
cpu_usage = EXCLUDED.cpu_usage,
mem_used_mb = EXCLUDED.mem_used_mb,
mem_total_mb = EXCLUDED.mem_total_mb,
capabilities = EXCLUDED.capabilities,
last_seen_at = NOW(),
last_seen = NOW()`,
[
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') {
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();
// Boot the temp-segment cleanup loop (runs hourly).
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;
async function gracefulShutdown(signal) {
if (_shuttingDown) return;
_shuttingDown = true;
console.log(`[shutdown] received ${signal} — closing gracefully…`);
// Stop accepting new requests + wind down the scheduler tick.
try { stopSchedulerLoop(); } catch (_) {}
// Force-exit watchdog so a hung connection can't keep us alive forever.
const killSwitch = setTimeout(() => {
console.error('[shutdown] forced exit after 25s timeout');
process.exit(1);
}, 25_000);
killSwitch.unref();
// Stop the HTTP server (waits for in-flight requests to finish).
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); }
console.log('[shutdown] clean exit');

View file

@ -64,6 +64,10 @@ const hlsQueue = new Queue('hls', {
connection: parseRedisUrl(process.env.REDIS_URL || 'redis://queue:6379'),
});
const promotionQueue = new Queue('promotion', {
connection: parseRedisUrl(process.env.REDIS_URL || 'redis://queue:6379'),
});
// GET / - List assets with filtering
router.get('/', async (req, res, next) => {
try {
@ -162,6 +166,7 @@ router.post('/', async (req, res, next) => {
capturedAt,
sourceType, // Bug #64: was ignored — now used to set media_type
needsProxy, // Bug #64: was ignored — now controls proxy queue logic
status, // 'live' when recording starts, 'processing' (default) when stopped
} = req.body;
if (!projectId || !clipName) {
@ -196,8 +201,8 @@ router.post('/', async (req, res, next) => {
let asset;
{
id = uuidv4();
// Bug #64: use sourceType to set media_type (default 'video')
const mediaType = (sourceType === 'audio') ? 'audio' : 'video';
const assetStatus = status || 'processing';
const ins = await pool.query(
`INSERT INTO assets (
id, project_id, bin_id,
@ -210,7 +215,7 @@ router.post('/', async (req, res, next) => {
VALUES (
$1, $2, $3,
$4, $4,
'processing', $9,
$10, $9,
$5, $6,
$7,
COALESCE($8::timestamptz, NOW()), NOW()
@ -223,6 +228,7 @@ router.post('/', async (req, res, next) => {
durationMs,
capturedAt || null,
mediaType,
assetStatus,
]
);
asset = ins.rows[0];
@ -230,9 +236,10 @@ router.post('/', async (req, res, next) => {
const thumbnailKey = `thumbnails/${id}.jpg`;
// Bug #64: when needsProxy is explicitly false and proxyKey is already set,
// skip re-queuing a proxy job and mark the asset ready immediately.
if (needsProxy === false && proxyKey) {
// Skip proxy/thumbnail queue for live assets - they'll be processed after recording stops
if (assetStatus === 'live') {
// Live assets stay in 'live' status until recording finishes
} else if (needsProxy === false && proxyKey) {
await pool.query(`UPDATE assets SET status = 'ready', updated_at = NOW() WHERE id = $1`, [id]);
asset.status = 'ready';
} else if (proxyKey) {
@ -505,6 +512,92 @@ router.post('/:id/finalize', requireAssetEdit, async (req, res, next) => {
} catch (err) { next(err); }
});
// POST /:id/pending-migration
// Capture sidecar calls this on a SUCCESSFUL growing-file recording stop.
// Flips the asset status from 'live' to 'pending_migration' (on SMB, not S3).
router.post('/:id/pending-migration', requireAssetEdit, async (req, res, next) => {
try {
const { id } = req.params;
const { duration } = req.body;
const check = await pool.query(`SELECT status FROM assets WHERE id = $1`, [id]);
if (check.rows.length === 0) return res.status(404).json({ error: 'Asset not found' });
if (check.rows[0].status !== 'live') {
return res.status(200).json({ skipped: true });
}
const durationNum = duration !== undefined && duration !== null ? Number(duration) : null;
const durationMs = (durationNum !== null && Number.isFinite(durationNum)) ? Math.round(durationNum * 1000) : null;
const upd = await pool.query(
`UPDATE assets
SET status = 'pending_migration',
duration_ms = COALESCE($2, duration_ms),
updated_at = NOW()
WHERE id = $1
RETURNING *`,
[id, durationMs]
);
console.log(`[assets] set pending-migration status for asset ${id}`);
res.json(upd.rows[0]);
} catch (err) { next(err); }
});
// POST /:id/promote
// Promotes an asset from 'pending_migration' (SMB) to S3.
// Enqueues a 'promotion' job in BullMQ to handle the S3 upload and metadata updates.
router.post('/:id/promote', requireAssetEdit, async (req, res, next) => {
try {
const { id } = req.params;
const check = await pool.query(`SELECT status FROM assets WHERE id = $1`, [id]);
if (check.rows.length === 0) return res.status(404).json({ error: 'Asset not found' });
const { status } = check.rows[0];
if (status !== 'pending_migration') {
return res.status(400).json({ error: `Asset status is "${status}" — only "pending_migration" assets can be promoted` });
}
// Update status to 'processing' so it is locked
await pool.query(
`UPDATE assets SET status = 'processing', updated_at = NOW() WHERE id = $1`,
[id]
);
// Queue the promotion job in BullMQ
await promotionQueue.add('promote', { assetId: id });
console.log(`[assets] queued promotion for asset ${id}`);
res.json({ ok: true, status: 'processing' });
} catch (err) { next(err); }
});
// POST /:id/live-thumbnail — set the poster thumbnail for a still-live asset.
// The capture sidecar extracts the first video frame from the first HLS segment
// (where the segment physically exists) and uploads it to S3, then calls this to
// record the key. This replaces the "connecting…" placeholder in the library with
// a real frame while recording is still in progress. Only touches thumbnail_s3_key
// — does NOT change status (the asset stays 'live' until the recording stops).
router.post('/:id/live-thumbnail', requireAssetEdit, async (req, res, next) => {
try {
const { id } = req.params;
const { thumbnailKey } = req.body;
if (!thumbnailKey) return res.status(400).json({ error: 'thumbnailKey is required' });
const upd = await pool.query(
`UPDATE assets SET thumbnail_s3_key = $2, updated_at = NOW()
WHERE id = $1 AND status = 'live'
RETURNING id, thumbnail_s3_key`,
[id, thumbnailKey]
);
if (upd.rows.length === 0) {
// Asset already finalized or gone — harmless, the post-stop thumbnail job covers it.
return res.status(200).json({ skipped: true });
}
res.json(upd.rows[0]);
} catch (err) { next(err); }
});
// POST /:id/generate-proxy
router.post('/:id/generate-proxy', requireAssetEdit, async (req, res, next) => {
try {
@ -742,11 +835,13 @@ router.get('/:id/live-path', async (req, res, next) => {
const cfg = {};
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' });
const rec = await pool.query(
`SELECT recording_container FROM recorders WHERE current_session_id = $1 ORDER BY updated_at DESC LIMIT 1`,
[asset.id]
);
const ext = rec.rows[0]?.recording_container || 'mov';
// The growing master is ALWAYS MXF OP1a (XDCAM HD422) on the share, regardless
// of the recorder's configured finalized container — that is the format
// Premiere supports for edit-while-record growing files (incremental index
// segments written into body partitions, readable with no footer). The file
// 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 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}`;

View file

@ -1,10 +1,27 @@
import express from 'express';
import http from 'http';
import os from 'os';
import pool from '../db/pool.js';
import { requireAdmin } from '../middleware/auth.js';
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
// 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
@ -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) => {
try {
const r = await pool.query(
@ -64,25 +80,45 @@ router.get('/', async (req, res, next) => {
FROM cluster_nodes
ORDER BY registered_at ASC`
);
res.json(r.rows.map(row => ({
...row,
online: Number(row.stale_seconds) < 120,
})));
res.json(r.rows.map(row => {
const out = { ...row, 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); }
});
// GET /containers list all containers on the local Docker host
router.get('/containers', async (req, res, next) => {
try {
const containers = await dockerRequest('/containers/json?all=true');
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 name = rawName.replace(/^wild-dragon-/, '').replace(/-\d+$/, '');
const ports = (c.Ports || [])
.filter(p => p.PublicPort)
.map(p => `${p.PublicPort}${p.PrivatePort}`)
.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 {
id: c.Id.slice(0, 12),
name,
@ -92,9 +128,9 @@ router.get('/containers', async (req, res, next) => {
healthy: (c.Status || '').includes('healthy'),
ports,
cpu: 0,
mem: 0,
memBytes,
};
});
}));
res.json(out);
} catch (err) {
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) => {
try {
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); }
});
// POST /heartbeat upsert this node's registration (includes hardware capabilities)
router.post('/heartbeat', async (req, res, next) => {
try {
const {
@ -122,11 +156,6 @@ router.post('/heartbeat', async (req, res, next) => {
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') {
const bound = req.tokenBoundHostname;
if (bound && bound !== hostname) {
@ -146,8 +175,8 @@ router.post('/heartbeat', async (req, res, next) => {
const r = await pool.query(
`INSERT INTO cluster_nodes
(hostname, ip_address, role, version, api_url,
cpu_usage, mem_used_mb, mem_total_mb, last_seen, capabilities, metadata, metrics)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,NOW(),$9,$10,$11)
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(),NOW(),$9,$10,$11)
ON CONFLICT (hostname) DO UPDATE SET
ip_address = EXCLUDED.ip_address,
role = EXCLUDED.role,
@ -157,6 +186,7 @@ router.post('/heartbeat', async (req, res, next) => {
mem_used_mb = EXCLUDED.mem_used_mb,
mem_total_mb = EXCLUDED.mem_total_mb,
last_seen = NOW(),
last_seen_at = NOW(),
capabilities = EXCLUDED.capabilities,
metadata = EXCLUDED.metadata,
metrics = COALESCE(EXCLUDED.metrics, cluster_nodes.metrics)
@ -179,42 +209,25 @@ router.post('/heartbeat', async (req, res, next) => {
} 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) => {
try {
// 1. Fetch all cluster nodes with DeckLink capabilities.
const nodesResult = await pool.query(
`SELECT id, hostname, ip_address, api_url, capabilities,
EXTRACT(EPOCH FROM (NOW() - last_seen)) AS stale_seconds
FROM cluster_nodes
WHERE capabilities IS NOT NULL`
);
// 2. Fetch all SDI recorders that are pinned to a node+device_index.
const recResult = await pool.query(
`SELECT id, name, status, container_id, node_id, device_index,
source_config
FROM recorders
WHERE source_type = 'sdi' AND node_id IS NOT NULL`
);
// Build a fast lookup: "${node_id}:${device_index}" → recorder row.
const recByPort = new Map();
for (const r of recResult.rows) {
const devIdx = r.device_index ?? r.source_config?.device ?? 0;
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 = [];
for (const node of nodesResult.rows) {
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 localHostname = process.env.NODE_HOSTNAME || '';
const isRemote = node.api_url && node.hostname !== localHostname;
bm.forEach((d, idx) => {
const portIndex = d.index !== undefined ? d.index : idx;
const rec = recByPort.get(`${node.id}:${portIndex}`);
tasks.push((async () => {
const base = {
node_id: node.id,
hostname: node.hostname,
index: portIndex,
device: d.device || null,
model,
node_online: nodeOnline,
recorder_id: rec ? rec.id : null,
recorder_name: rec ? rec.name : null,
node_id: node.id, hostname: node.hostname, index: portIndex,
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,
signal: 'no-recorder',
framesReceived: null,
currentFps: null,
signal: 'no-recorder', framesReceived: null, currentFps: null,
};
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';
return base;
}
// Active recording — query the capture container for real signal.
try {
let live = null;
if (isRemote) {
const r = await fetch(
`${node.api_url}/sidecar/${rec.container_id}/status`,
{ signal: AbortSignal.timeout(2500) }
);
const r = await fetch(`${node.api_url}/sidecar/${rec.container_id}/status`, { signal: AbortSignal.timeout(2500) });
if (r.ok) live = (await r.json()).live;
} else {
const r = await fetch(
`http://recorder-${rec.id}:3001/capture/status`,
{ signal: AbortSignal.timeout(2000) }
);
const r = await fetch(`http://recorder-${rec.id}:3001/capture/status`, { signal: AbortSignal.timeout(2000) });
if (r.ok) live = await r.json();
}
if (live && live.signal) {
base.signal = live.signal;
base.signal = live.signal;
base.framesReceived = live.framesReceived ?? null;
base.currentFps = live.currentFps ?? null;
} else {
base.signal = 'connecting';
}
} catch (_) {
base.signal = 'connecting';
}
base.currentFps = live.currentFps ?? null;
} else { base.signal = 'connecting'; }
} catch (_) { base.signal = 'connecting'; }
return base;
})());
});
}
const results = await Promise.all(tasks);
res.json(results);
} 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) => {
try {
const r = await pool.query(
`SELECT id, hostname, ip_address, role, capabilities,
EXTRACT(EPOCH FROM (NOW() - last_seen)) AS stale_seconds
FROM cluster_nodes
WHERE capabilities IS NOT NULL`
FROM cluster_nodes WHERE capabilities IS NOT NULL`
);
const out = [];
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 model = (row.capabilities && row.capabilities.blackmagic_model) || null;
bm.forEach((d, idx) => {
out.push({
node_id: row.id,
hostname: row.hostname,
ip_address: row.ip_address,
role: row.role,
online,
model,
index: d.index !== undefined ? d.index : idx,
device: d.device,
});
out.push({ node_id: row.id, 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);
} 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) => {
try {
const r = await pool.query(
`SELECT id, hostname, ip_address, role, capabilities,
EXTRACT(EPOCH FROM (NOW() - last_seen)) AS stale_seconds
FROM cluster_nodes
WHERE capabilities IS NOT NULL`
FROM cluster_nodes WHERE capabilities IS NOT NULL`
);
const out = [];
for (const row of r.rows) {
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;
// 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) => {
out.push({
node_id: row.id,
hostname: row.hostname,
ip_address: row.ip_address,
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,
});
out.push({ node_id: row.id, hostname: row.hostname, ip_address: row.ip_address,
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);
} 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) => {
try {
const [nodesRes, recordersRes] = await Promise.all([
pool.query(
`SELECT id, hostname, ip_address, api_url, capabilities,
pool.query(`SELECT id, hostname, ip_address, api_url, capabilities,
EXTRACT(EPOCH FROM (NOW() - last_seen)) AS stale_seconds
FROM cluster_nodes
WHERE capabilities IS NOT NULL`
),
pool.query(
`SELECT id, node_id, device_index, status, source_type, container_id
FROM recorders WHERE source_type = 'deltacast'`
),
FROM cluster_nodes WHERE capabilities IS NOT NULL`),
pool.query(`SELECT id, node_id, device_index, status, source_type, container_id
FROM recorders WHERE source_type = 'deltacast'`),
]);
const recByNodePort = {};
for (const rec of recordersRes.rows) {
recByNodePort[`${rec.node_id}:${rec.device_index}`] = rec;
}
const results = [];
const fetchPromises = [];
for (const node of nodesRes.rows) {
const online = Number(node.stale_seconds) < 120;
const dc = (node.capabilities && node.capabilities.deltacast) || [];
const model = (node.capabilities && node.capabilities.deltacast_model) || 'Deltacast';
for (const port of dc) {
const idx = port.index !== undefined ? port.index : dc.indexOf(port);
const rec = recByNodePort[`${node.id}:${idx}`];
const base = {
node_id: node.id,
hostname: node.hostname,
ip_address: node.ip_address,
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,
};
const base = { node_id: node.id, hostname: node.hostname, ip_address: node.ip_address,
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.status !== 'recording') { base.signal = 'idle'; results.push(base); continue; }
// Active recording — query capture container for real signal.
const fetchIdx = results.length;
results.push(base);
fetchPromises.push((async () => {
try {
const url = node.api_url
? `${node.api_url}/sidecar/${rec.container_id}/status`
const url = node.api_url ? `${node.api_url}/sidecar/${rec.container_id}/status`
: `http://recorder-${rec.id}:3001/capture/status`;
const r = await fetch(url, { signal: AbortSignal.timeout(2500) });
if (r.ok) {
const live = await r.json();
if (live && live.signal) {
results[fetchIdx].signal = live.signal;
results[fetchIdx].signal = live.signal;
results[fetchIdx].framesReceived = live.framesReceived ?? null;
results[fetchIdx].currentFps = live.currentFps ?? null;
results[fetchIdx].currentFps = live.currentFps ?? null;
}
}
} catch (_) {
results[fetchIdx].signal = 'connecting';
}
} catch (_) { results[fetchIdx].signal = 'connecting'; }
})());
}
}
await Promise.all(fetchPromises);
res.json(results);
} catch (err) { next(err); }
});
// GET /:id/ping probe the node's api_url/health endpoint directly
router.get('/:id/ping', async (req, res, next) => {
try {
const r = await pool.query(
'SELECT id, hostname, api_url FROM cluster_nodes WHERE id = $1',
[req.params.id]
);
const r = await pool.query('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' });
const node = r.rows[0];
if (!node.api_url) return res.json({ reachable: false, reason: 'no api_url registered' });
const start = Date.now();
try {
const upstream = await fetch(`${node.api_url}/health`, {
signal: AbortSignal.timeout(4000),
});
const upstream = await fetch(`${node.api_url}/health`, { signal: AbortSignal.timeout(4000) });
const latency_ms = Date.now() - start;
const body = await upstream.json().catch(() => ({}));
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); }
});
// ── 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) => {
try {
const r = await pool.query(
@ -471,59 +472,37 @@ router.get('/metrics', async (req, res, next) => {
cpu_usage, mem_used_mb, mem_total_mb,
capabilities, metrics,
EXTRACT(EPOCH FROM (NOW() - last_seen)) AS stale_seconds
FROM cluster_nodes
ORDER BY registered_at ASC`
FROM cluster_nodes ORDER BY registered_at ASC`
);
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 gpus = capGpus.map((g, idx) => {
const live = liveGpus.find(l => l.index === g.index) || liveGpus[idx] || {};
return {
name: g.name || null,
util_pct: live.util_pct != null ? live.util_pct : 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),
};
return { name: g.name || null, util_pct: live.util_pct != null ? live.util_pct : 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) {
if (!capGpus.some(g => g.index === lg.index)) {
gpus.push({
name: lg.name || null,
util_pct: lg.util_pct != null ? lg.util_pct : 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,
});
gpus.push({ name: lg.name || null, util_pct: lg.util_pct != null ? lg.util_pct : 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,
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,
};
return { id: row.id, hostname: row.hostname, role: row.role,
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 });
} catch (err) { next(err); }
});
// DELETE /:id deregister a node
router.delete('/:id', async (req, res, next) => {
try {
const r = await pool.query(
'DELETE FROM cluster_nodes WHERE id = $1 RETURNING id',
[req.params.id]
);
const r = await pool.query('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' });
res.json({ ok: true });
} catch (err) { next(err); }

View file

@ -21,7 +21,6 @@ import {
const router = express.Router();
// ── BullMQ: media staging queue (S3 -> /media volume) ────────────────────────
const parseRedisUrl = (url) => {
const parsed = new URL(url);
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'),
});
// ── Sidecar orchestration (mirrors recorders.js) ─────────────────────────────
const PLAYOUT_SIDECAR_IMAGE = process.env.PLAYOUT_IMAGE || 'wild-dragon-playout:latest';
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 };
}
// 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;
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) {
if (channel.container_meta && 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(() => ({}));
}
// ── Serialization ────────────────────────────────────────────────────────────
function channelToJson(r) {
return {
id: r.id,
@ -123,7 +114,6 @@ function channelToJson(r) {
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) => {
validateUuid('id')(req, res, () => {});
if (res.headersSent) return;
@ -143,9 +133,6 @@ async function requireChannelEdit(req, res, next) {
catch (err) { next(err); }
}
// ── Channels ─────────────────────────────────────────────────────────────────
// GET /playout/channels — list (filtered to accessible projects)
router.get('/channels', async (req, res, next) => {
try {
let rows;
@ -162,7 +149,6 @@ router.get('/channels', async (req, res, next) => {
} catch (err) { next(err); }
});
// POST /playout/channels — create
router.post('/channels', async (req, res, next) => {
try {
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)) {
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');
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); }
});
// PATCH /playout/channels/:id — update config (only while stopped)
router.patch('/channels/:id', requireChannelEdit, async (req, res, next) => {
try {
if (req.channel.status === 'running') {
@ -214,7 +197,6 @@ router.patch('/channels/:id', requireChannelEdit, async (req, res, next) => {
} catch (err) { next(err); }
});
// DELETE /playout/channels/:id
router.delete('/channels/:id', requireChannelEdit, async (req, res, next) => {
try {
if (req.channel.status === 'running') {
@ -225,14 +207,9 @@ router.delete('/channels/:id', requireChannelEdit, async (req, res, next) => {
} 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) {
if (channel.output_type !== 'decklink') return;
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(
`SELECT id FROM playout_channels
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) {
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(
`SELECT id FROM recorders
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) {
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 || {})}`,
`VIDEO_FORMAT=${channel.video_format}`,
`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}`,
];
@ -301,7 +268,6 @@ async function spawnChannelSidecar(channel) {
}
const data = await sidecarRes.json();
containerId = data.containerId;
// node-agent returns the reachable host:port the shim is published on.
if (data.sidecarUrl || data.host) {
containerMeta.sidecar_url = data.sidecarUrl || `http://${data.host}:${SIDECAR_HTTP_PORT}`;
}
@ -314,7 +280,10 @@ async function spawnChannelSidecar(channel) {
Image: PLAYOUT_SIDECAR_IMAGE,
Env: env,
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,
Binds: hostBinds,
},
@ -353,7 +322,6 @@ async function spawnChannelSidecar(channel) {
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) => {
try {
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) => {
try {
const channel = req.channel;
@ -394,7 +361,6 @@ router.post('/channels/:id/stop', requireChannelEdit, async (req, res, next) =>
} catch (err) { next(err); }
});
// GET /playout/channels/:id/status — live engine status (proxied to sidecar)
router.get('/channels/:id/status', async (req, res, next) => {
try {
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 }); }
}
// 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) => {
try {
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/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) => {
try {
const { rows } = await pool.query(
@ -513,10 +476,118 @@ router.get('/channels/:id/asrun', async (req, res, next) => {
} 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) {
// 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;
if (!channelId) return res.status(400).json({ error: 'channel_id is required' });
try {
@ -528,7 +599,6 @@ async function loadChannelForBody(req, res, next) {
} catch (err) { next(err); }
}
// GET /playout/playlists?channel_id=...
router.get('/playlists', async (req, res, next) => {
try {
const channelId = req.query.channel_id;
@ -542,7 +612,6 @@ router.get('/playlists', async (req, res, next) => {
} catch (err) { next(err); }
});
// POST /playout/playlists
router.post('/playlists', loadChannelForBody, async (req, res, next) => {
try {
const { name, loop = false } = req.body || {};
@ -554,7 +623,6 @@ router.post('/playlists', loadChannelForBody, async (req, res, next) => {
} catch (err) { next(err); }
});
// GET /playout/playlists/:plid/items
router.get('/playlists/:plid/items', validateUuid('plid'), async (req, res, next) => {
try {
const pl = await pool.query(
@ -570,7 +638,6 @@ router.get('/playlists/:plid/items', validateUuid('plid'), async (req, res, next
} catch (err) { next(err); }
});
// Helper: load a playlist + assert edit on its channel's project.
async function loadPlaylistEdit(plid, user) {
const pl = await pool.query(
`SELECT p.*, c.project_id FROM playout_playlists p
@ -580,7 +647,6 @@ async function loadPlaylistEdit(plid, user) {
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) => {
try {
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 || {};
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(
'SELECT COALESCE(MAX(sort_order), -1) + 1 AS next FROM playout_items WHERE playlist_id = $1',
[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 *`,
[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) =>
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) => {
const client = await pool.connect();
try {
@ -631,7 +693,6 @@ router.put('/playlists/:plid/reorder', validateUuid('plid'), async (req, res, ne
} finally { client.release(); }
});
// DELETE /playout/items/:itemId
router.delete('/items/:itemId', validateUuid('itemId'), async (req, res, next) => {
try {
const it = await pool.query(
@ -645,7 +706,6 @@ router.delete('/items/:itemId', validateUuid('itemId'), async (req, res, next) =
} 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) => {
try {
const it = await pool.query(
@ -660,13 +720,6 @@ router.post('/items/:itemId/stage', validateUuid('itemId'), async (req, res, nex
} 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) {
const { rows } = await pool.query('SELECT * FROM playout_channels WHERE id = $1', [channelId]);
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' };
}
// Best-effort teardown of the old container — it may already be dead.
if (channel.container_id) {
const { remote, apiUrl } = await resolveNodeTarget(channel.node_id);
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(
`SELECT id, hostname, api_url, last_seen_at FROM cluster_nodes
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;
// 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(
`UPDATE playout_channels
SET node_id = $1, status = 'stopped', container_id = NULL, container_meta = '{}'::jsonb,
@ -720,10 +766,6 @@ export async function restartChannel(channelId) {
[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 {
await spawnChannelSidecar(moved[0]);
return { restarted: true, new_node_id: newNodeId };

View file

@ -179,6 +179,94 @@ function pickRecorderFields(body) {
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
//
// Issue #121 — previous version fired N PG queries + N Docker inspects per
@ -228,6 +316,15 @@ router.get('/', async (req, res, next) => {
} catch (_) { /* leave started_at undefined */ }
}));
// Append preview_url for deltacast/sdi recorders whose sidecar is running.
// 1 fps JPEG confidence snapshot (frame.jpg) — does NOT compete with the
// recorder for the video FIFO (a 2nd continuous reader halves capture fps).
for (const r of rows) {
if (r.container_id && (r.source_type === 'deltacast' || r.source_type === 'sdi')) {
r.preview_url = `/api/v1/recorders/${r.id}/preview/frame.jpg`;
}
}
res.json(rows);
} catch (err) {
next(err);
@ -269,6 +366,13 @@ router.post('/', async (req, res, next) => {
};
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.
const cols = Object.keys(row);
const placeholders = cols.map((_, i) => `$${i + 1}`).join(', ');
@ -335,6 +439,15 @@ router.patch('/:id', requireRecorderEdit, async (req, res, next) => {
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 params = cols.map(k => fields[k]);
params.push(id);
@ -422,9 +535,17 @@ router.post('/:id/start', requireRecorderEdit, async (req, res, next) => {
// live-asset: create the asset row right now (status='live') so the
// library shows the recording while it is happening.
//
// CRITICAL: the original_s3_key extension MUST match what the capture
// sidecar actually produces, or the post-stop proxy/promotion worker
// downloads a nonexistent object and the asset goes to 'error'.
// - growing-files ON → capture-manager writes a growing OP1a/RDD-9 MXF
// (GROWING_EXT = 'mxf'), uploaded by the promotion worker. So the key
// MUST be .mxf regardless of the recorder's configured container.
// - growing-files OFF → ffmpeg muxes the configured container (mov/mp4…).
const assetIdLive = uuidv4();
try {
const ext = recorder.recording_container || 'mov';
const ext = recorder.growing_enabled ? 'mxf' : (recorder.recording_container || 'mov');
await pool.query(
`INSERT INTO assets (
id, project_id, bin_id, filename, display_name, status, media_type,
@ -510,11 +631,15 @@ router.post('/:id/start', requireRecorderEdit, async (req, res, next) => {
}
// GPU-accelerated codecs require the NVIDIA container runtime on the node.
// hevc_nvenc / h264_nvenc are the only two we currently support; extend
// this list if av1_nvenc or others are added later.
const GPU_CODECS = ['hevc_nvenc', 'h264_nvenc'];
// hevc_nvenc / h264_nvenc are the only two we currently support (see the
// module-level GPU_CODECS list); extend it if av1_nvenc or others are added.
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.
const { remote: isRemote, apiUrl: targetNodeApiUrl } = await resolveNodeTarget(recorder.node_id);
// For remote sidecars, the capture container runs on the worker host network and cannot
@ -532,7 +657,7 @@ router.post('/:id/start', requireRecorderEdit, async (req, res, next) => {
const sidecarRes = await fetch(`${targetNodeApiUrl}/sidecar/start`, {
method: 'POST',
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),
});
if (!sidecarRes.ok) {
@ -575,7 +700,8 @@ router.post('/:id/start', requireRecorderEdit, async (req, res, next) => {
const localEnv = [...env];
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');
}
@ -677,33 +803,22 @@ router.post('/:id/stop', requireRecorderEdit, async (req, res, next) => {
return res.status(502).json({ error: 'Remote node failed to stop sidecar' });
}
} else {
const stopRes = await dockerApi(
'POST',
`/containers/${recorder.container_id}/stop`
);
// 204 = stopped, 304 = already stopped, 404 = container gone — all acceptable.
if (stopRes.status !== 204 && stopRes.status !== 304 && stopRes.status !== 404) {
return res.status(500).json({
error: 'Failed to stop container',
details: stopRes.data,
});
}
// Only attempt remove if the container existed (not 404).
if (stopRes.status !== 404) {
const removeRes = await dockerApi(
'DELETE',
`/containers/${recorder.container_id}`
);
if (removeRes.status !== 204 && removeRes.status !== 404) {
return res.status(500).json({
error: 'Failed to remove container',
details: removeRes.data,
});
// Issue #162 — stop local container in the background so the HTTP stop
// request returns immediately. The container teardown (SIGTERM -> ffmpeg
// exit -> S3 upload -> post-stop callback) takes up to 180s for large files,
// which would otherwise timeout the browser/API connection.
const containerId = recorder.container_id;
(async () => {
try {
const stopRes = await dockerApi('POST', `/containers/${containerId}/stop?t=180`);
if (stopRes.status !== 404) {
await waitForFinalize(recorder);
await dockerApi('DELETE', `/containers/${containerId}?force=true`).catch(() => {});
}
} catch (e) {
console.error('[recorders] failed local background stop:', e.message);
}
}
})();
}
// ── Growing-files S3 promotion ────────────────────────────────────────────
@ -1105,6 +1220,36 @@ function probeUdp(host, port) {
}
// GET /:id/preview/* — proxy idle signal preview HLS from /live/preview-{id}/ on the recorder's node.
router.get('/:id/preview/:rest(*)', async (req, res, next) => {
try {
const { id } = req.params;
const rest = req.params.rest;
if (!rest || rest.includes('..')) return res.status(400).end();
const rec = await pool.query('SELECT node_id FROM recorders WHERE id = $1', [id]);
if (rec.rows.length === 0) return res.status(404).json({ error: 'Recorder not found' });
const ct = rest.endsWith('.m3u8') ? 'application/vnd.apple.mpegurl'
: rest.endsWith('.ts') ? 'video/mp2t'
: rest.endsWith('.jpg') ? 'image/jpeg'
: 'application/octet-stream';
res.set('Cache-Control', 'no-cache');
res.set('Content-Type', ct);
const target = await resolveNodeTarget(rec.rows[0].node_id);
if (!target.remote) {
return fs.readFile(`/live/preview-${id}/${rest}`, (err, data) => {
if (err) return res.status(404).end();
res.end(data);
});
}
const base = String(target.apiUrl).replace(/\/$/, '');
const upstream = await fetch(`${base}/live/preview-${id}/${rest}`).catch(() => null);
if (!upstream || !upstream.ok) return res.status(upstream ? upstream.status : 502).end();
res.end(Buffer.from(await upstream.arrayBuffer()));
} catch (err) { next(err); }
});
// GET /:id/live/* — reverse-proxy the live HLS preview from the recorder's node.
// Remote recorders: segments live on the worker node, served by its node-agent
// (/live/...). Local recorders: served from this host's /live mount. Browser

View file

@ -3,10 +3,12 @@
import express from 'express';
import pool from '../db/pool.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 MIN_PASSWORD_LEN = 12;
const ROLES = ['admin', 'editor', 'viewer'];
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) => {
try {
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]);
res.json(rows);
} catch (err) { next(err); }
@ -26,6 +28,7 @@ router.post('/', async (req, res, next) => {
const { username, password, display_name, role } = req.body || {};
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 (role !== undefined && !ROLES.includes(role)) return bad(res, "role must be one of: " + ROLES.join(', '));
const hash = await hashPassword(password);
const { rows } = await pool.query(
`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' });
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?.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 (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()');
@ -93,4 +99,88 @@ router.patch('/:id', async (req, res, next) => {
} 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;

View file

@ -9,7 +9,7 @@
import pool from './db/pool.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';
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(() => ({}));
}
// Issue #103 — every mam-api replica runs the same tick on the same interval,
// 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
const SCHEDULER_LOCK_KEY = 8210301;
async function tryAcquireSchedulerLock(client) {
const r = await client.query('SELECT pg_try_advisory_lock($1) AS got', [SCHEDULER_LOCK_KEY]);
@ -57,14 +53,9 @@ async function tick() {
try {
haveLock = await tryAcquireSchedulerLock(client);
if (!haveLock) {
// Another replica is processing this interval — bail silently.
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(
`UPDATE recorder_schedules
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(
`UPDATE recorder_schedules
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}`);
await enqueueNextOccurrence(s, client);
} catch (err) {
// Stop failed — flag as failed but don't keep trying forever.
await client.query(
`UPDATE recorder_schedules
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(
`SELECT s.* FROM recorder_schedules s
JOIN recorders r ON r.id = s.recorder_id
@ -147,9 +135,24 @@ 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.
// Orphaned live assets: recorder stopped but asset still 'live'.
// Happens when the capture sidecar crashes before finalize() runs.
// Mark error immediately so the library doesn't show "Recording" forever.
const orphanResult = await client.query(
`UPDATE assets a
SET status = 'error', updated_at = NOW()
FROM recorders r
WHERE a.status = 'live'
AND a.display_name = r.current_session_id
AND r.status = 'stopped'
RETURNING a.id, a.display_name`
);
if (orphanResult.rows.length > 0) {
for (const row of orphanResult.rows) {
console.warn(`[scheduler] orphaned live asset (recorder stopped): ${row.id} (${row.display_name})`);
}
}
const LIVE_TIMEOUT_MINUTES = parseInt(process.env.LIVE_ASSET_TIMEOUT_MINUTES || '120', 10);
const staleResult = await client.query(
`UPDATE assets
@ -166,9 +169,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(
`SELECT id, project_id, bin_id FROM assets
WHERE ampp_sync_status IN ('pending', 'failed')
@ -181,11 +181,6 @@ async function tick() {
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);
} catch (err) {
console.error('[scheduler] tick error:', err);
@ -285,7 +280,6 @@ async function playoutHealthTick(client) {
FROM playout_channels WHERE status = 'running'`
));
} catch (err) {
// Migration 029 may not be applied yet — bail silently rather than crash.
if (err.code === '42P01') return;
throw err;
}
@ -313,6 +307,32 @@ async function playoutHealthTick(client) {
} catch (e) {
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) {
// When last_heartbeat_at is NULL (channel just spawned), fall back to
// updated_at (set to NOW() by spawnChannelSidecar). This prevents a
@ -321,7 +341,7 @@ async function playoutHealthTick(client) {
const baseline = ch.last_heartbeat_at || ch.updated_at;
const lastSeen = baseline ? new Date(baseline).getTime() : Date.now();
const ageMs = Date.now() - lastSeen;
if (ageMs < TIMEOUT_MS) continue; // not yet 3 misses
if (ageMs < TIMEOUT_MS) continue;
if (ch.output_type === 'decklink') {
await client.query(
@ -334,8 +354,6 @@ async function playoutHealthTick(client) {
console.warn(`[scheduler] failover: channel ${ch.id} unreachable (${err.message}), restart #${ch.restart_count + 1}`);
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);
if (res.restarted) {
console.log(`[scheduler] failover: channel ${ch.id} re-placed on node ${res.new_node_id}`);
@ -352,8 +370,6 @@ async function playoutHealthTick(client) {
export function startSchedulerLoop() {
if (_interval) return;
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);
_interval = setInterval(() => tick().catch(() => {}), TICK_INTERVAL_MS);
}

View file

@ -1,14 +1,175 @@
import http from 'http';
import os from 'os';
import fs from 'fs';
import http from 'http';
import os from 'os';
import fs from 'fs';
import { spawn } from 'child_process';
const MAM_API_URL = (process.env.MAM_API_URL || 'http://localhost:3000').replace(/\/$/, '');
const NODE_TOKEN = process.env.NODE_TOKEN || '';
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 HEARTBEAT_MS = parseInt(process.env.HEARTBEAT_MS || '30000', 10);
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;
}
// Check /proc on Linux to see if a deltacast-bridge process is alive.
// Used by startDeltacastBridge() to detect a bridge started outside node-agent
// (e.g. manually with sudo, or from a prior node-agent process).
function _dcBridgeProcessAlive() {
try {
for (const pid of fs.readdirSync('/proc')) {
if (!/^\d+$/.test(pid)) continue;
try {
// cmdline is NUL-delimited; read as binary-friendly string.
const cmdline = fs.readFileSync(`/proc/${pid}/cmdline`, 'latin1');
if (cmdline.includes('deltacast-bridge')) return true;
} catch (_) { /* process may have exited mid-scan */ }
}
} catch (_) {}
return false;
}
function startDeltacastBridge() {
if (_dcBridgeRunning()) return; // already up (we spawned it)
try { fs.mkdirSync(DC_PIPE_DIR, { recursive: true }); } catch (_) {}
// FIFOs may exist from a previous run. Only skip the spawn if a
// deltacast-bridge process is actually alive on the host — stale FIFOs with
// no live writer cause ffmpeg to block on open() indefinitely (no audio/video).
const _v0 = DC_PIPE_DIR + '/video-0.fifo';
if (fs.existsSync(_v0)) {
if (_dcBridgeProcessAlive()) {
console.log('[dc-bridge] FIFOs exist and bridge process alive — skipping spawn');
return;
}
console.log('[dc-bridge] FIFOs exist but bridge is NOT running — spawning fresh bridge');
// Stale FIFOs are harmless: the bridge recreates them (mkfifo ignores EEXIST).
}
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('error', (err) => {
console.error(`[dc-bridge] spawn error: ${err.message} (binary=${DC_BRIDGE_BIN})`);
_dcBridge = null;
});
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,
// os.networkInterfaces() returns the container's docker-bridge IP (172.x),
@ -93,6 +254,11 @@ async function handleSidecarStart(body, res) {
// (ProRes, DNxHR, libx264) don't need it and it avoids a hard dep on the
// NVIDIA container runtime on nodes that have no GPU.
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;
const binds = [`${LIVE_DIR}:/live`];
@ -103,17 +269,22 @@ async function handleSidecarStart(body, res) {
try {
const dcEntries = fs.readdirSync('/dev').filter(n => /^deltacast\d+$/.test(n));
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 */ }
}
// Build the sidecar environment, injecting NVIDIA vars when GPU is requested.
const sidecarEnv = [...env, `PORT=${capturePort}`];
if (useGpu) {
// NVIDIA_VISIBLE_DEVICES=all exposes every GPU on the host.
// For a single-GPU node (zampp2 / L4) this is equivalent to pinning GPU 0.
// When we later store per-recorder GPU affinity in the DB we can pass a
// specific UUID here instead.
sidecarEnv.push('NVIDIA_VISIBLE_DEVICES=all');
// Issue #167 — per-recorder GPU affinity. A gpuUuid (UUID string or
// numeric index) pins the sidecar to exactly that device; otherwise
// NVIDIA_VISIBLE_DEVICES=all exposes every GPU on the host (legacy
// behavior — for a single-GPU node like zampp2 / L4 this equals GPU 0).
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');
}
@ -137,22 +308,60 @@ async function handleSidecarStart(body, res) {
HostConfig: hostConfig,
};
const createRes = await dockerApi('POST', '/containers/create', spec);
if (createRes.status !== 201) {
return jsonResponse(res, 502, { error: 'Failed to create container', details: createRes.data });
// Deltacast: ensure the shared bridge daemon is running on the HOST before
// starting the sidecar. The sidecar reads FIFOs produced by the bridge;
// 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;
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) {
await dockerApi('DELETE', `/containers/${containerId}?force=true`).catch(() => {});
return jsonResponse(res, 502, { error: 'Failed to start container', details: startRes.data });
}
let containerId;
try {
const createRes = await dockerApi('POST', '/containers/create', spec);
if (createRes.status !== 201) {
if (sourceType === 'deltacast') {
_dcSidecarCount--;
if (_dcSidecarCount <= 0) { _dcSidecarCount = 0; stopDeltacastBridge(); }
}
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) {
jsonResponse(res, 500, { error: err.message });
}
@ -178,17 +387,34 @@ async function fetchContainerLogs(containerId) {
async function handleSidecarStop(containerId, res) {
try {
console.log(`[sidecar-stop] stopping ${containerId} (grace 180s)...`);
// Grace period must exceed the capture container's shutdown work
// (finalise ffmpeg session + register asset via callback). Default
// docker stop is only 10s, which SIGKILLs capture mid-finalise and
// loses the POST /assets callback -> asset stuck 'live', no jobs.
await dockerApi('POST', `/containers/${containerId}/stop?t=180`).catch(() => {});
// Dump the capture container's shutdown logs into our persistent log
// BEFORE removing it, so failed callbacks are diagnosable.
const logs = await fetchContainerLogs(containerId);
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.
await dockerApi('DELETE', `/containers/${containerId}?force=true`).catch(() => {});
// Run the container teardown and cleanup in the background. The capture
// process SIGTERM handler flushes ffmpeg and uploads the file to S3
// (taking up to 3 minutes for multi-GB files) before exiting. Returning
// immediately stops the API request timing out.
(async () => {
try {
await dockerApi('POST', `/containers/${containerId}/stop?t=180`).catch(() => {});
const logs = await fetchContainerLogs(containerId);
console.log(`[sidecar-stop] ==== capture logs for ${containerId} ====\n${logs}\n[sidecar-stop] ==== end logs ====`);
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);
}
} catch (err) {
console.error(`[sidecar-stop] background cleanup failed for ${containerId}:`, err.message);
}
})();
jsonResponse(res, 200, { ok: true });
} catch (err) {
console.error(`[sidecar-stop] error: ${err.message}`);
@ -227,6 +453,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) ───────────────────────────────────────────
function sampleCpu() {
return new Promise(resolve => {
@ -247,21 +614,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.
// Returns array of { index, util_pct, mem_used_mb, mem_total_mb } per GPU,
// or [] if no GPUs / nvidia runtime unavailable.
// Returns array of { index, util_pct, enc_util_pct, mem_used_mb, mem_total_mb,
// 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() {
if (!_gpuCache || _gpuCache.length === 0) return [];
const QUERY = '--query-gpu=index,utilization.gpu,memory.used,memory.total';
const FMT = '--format=csv,noheader,nounits';
const GPU_QUERY = '--query-gpu=index,utilization.gpu,utilization.encoder,memory.used,memory.total';
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;
try {
const createRes = await dockerApi('POST', '/containers/create', {
Image: 'ubuntu:22.04',
Cmd: ['nvidia-smi', QUERY, FMT],
Cmd: ['sh', '-c', SCRIPT],
HostConfig: {
AutoRemove: false,
Runtime: 'nvidia',
@ -295,11 +680,46 @@ async function sampleGpuUtil() {
});
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 => {
const [idx, util, memUsed, memTotal] = line.split(',').map(s => parseInt(s.trim(), 10));
return { index: idx, util_pct: util, mem_used_mb: memUsed, mem_total_mb: memTotal };
// utilization.encoder may report "[N/A]" on cards/drivers that don't
// 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) {
console.warn('[gpu-util] sampling failed:', err.message);
@ -480,12 +900,31 @@ async function heartbeat() {
const ip_address = getIp();
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 = {
hostname: os.hostname(),
hostname: NODE_NAME,
ip_address,
role: NODE_ROLE,
version: VERSION,
api_url: `http://${ip_address || os.hostname()}:${AGENT_PORT}`,
api_url: `http://${ip_address || NODE_NAME}:${AGENT_PORT}`,
cpu_usage,
mem_used_mb: Math.round((totalMem - freeMem) / 1048576),
mem_total_mb: Math.round(totalMem / 1048576),
@ -572,6 +1011,16 @@ const server = http.createServer((req, res) => {
const id = pathname.slice('/sidecar/'.length, -'/status'.length);
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/')) {
serveLiveFile(pathname, res);

View file

@ -78,10 +78,6 @@ RUN set -eux; \
echo "caspar binary: /opt/casparcg_server/bin/casparcg"; \
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 \
mkdir -p /opt/ndi-lib && \
curl -fsSL "$NDI_SDK_URL" -o /tmp/ndi.tar.gz && \
@ -91,16 +87,13 @@ RUN if [ -n "$NDI_SDK_URL" ]; then \
fi
ENV NDI_RUNTIME_DIR_V6=/opt/ndi-lib
# CasparCG media folder — mam-api stages assets from S3 into this volume.
RUN mkdir -p /media
# ── Node control shim ────────────────────────────────────────────────────────
WORKDIR /app
COPY package*.json ./
RUN npm install --omit=dev
COPY . .
# CasparCG config + entrypoint
COPY casparcg.config /opt/casparcg/casparcg.config
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

View file

@ -1,8 +1,6 @@
#!/usr/bin/env bash
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
echo "[entrypoint] starting Xvfb on :99"
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/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()));
// 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 { 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.
//
@ -83,8 +83,13 @@ export class PlayoutManager {
currentClip: null,
startedAt: 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._scteTimer = null;
this._hlsProc = null; // standalone ffmpeg re-mux child process
this._hlsRestartTimer = null;
}
@ -211,6 +216,20 @@ export class PlayoutManager {
_startHlsRemux() {
if (!HLS_DIR) return;
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();
const out = `${HLS_DIR}/index.m3u8`;
@ -291,6 +310,7 @@ export class PlayoutManager {
async stopChannel() {
this._clearAdvance();
this._clearScte();
this.state.running = false; // set first so the ffmpeg exit handler won't respawn
this._stopHlsRemux();
try { await this.amcp.send(`STOP ${CHANNEL}-${FG_LAYER}`); } catch (_) {}
@ -416,6 +436,92 @@ export class PlayoutManager {
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) {
// 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
@ -437,6 +543,7 @@ export class PlayoutManager {
loop: this.state.loop,
startedAt: this.state.startedAt,
lastError: this.state.lastError,
scteActive: this.state.scteActive || null,
};
}
}

View file

@ -2,7 +2,7 @@
"manifestVersion": 5,
"id": "net.wilddragon.dragonflight.uxp",
"name": "Dragonflight MAM",
"version": "2.2.2",
"version": "2.2.3",
"main": "index.html",
"host": {
"app": "premierepro",

View file

@ -1,14 +1,29 @@
// import-flow.js — v2.1.6
// import-flow.js — v2.2.3
// premierepro API: docs say sync, runtime returns Promises. Await everything.
(function () {
const Import = {};
const fs = require('fs');
const fsPromises = fs.promises || {};
// window.path is a UXP global (v6.4+) — no require('path')
let os; try { os = require('os'); } catch (_) { os = {}; }
let uxpFs; try { uxpFs = require('uxp').storage.localFileSystem; } catch (_) { uxpFs = null; }
// UXP fs is callback-based — wrap in Promise where promisify unavailable.
function _writeFile(path, data) {
if (fsPromises.writeFile) return fsPromises.writeFile(path, data);
return new Promise((resolve, reject) => { fs.writeFile(path, data, (err) => { if (err) reject(err); else resolve(); }); });
}
function _readFile(path) {
if (fsPromises.readFile) return fsPromises.readFile(path);
return new Promise((resolve, reject) => { fs.readFile(path, (err, data) => { if (err) reject(err); else resolve(data); }); });
}
function _stat(path) {
if (fsPromises.stat) return fsPromises.stat(path);
return new Promise((resolve, reject) => { fs.stat(path, (err, stats) => { if (err) reject(err); else resolve(stats); }); });
}
// ── Temp folder ──────────────────────────────────────────────────
async function _getTempBase() {
if (uxpFs && uxpFs.getTemporaryFolder) {
@ -39,7 +54,7 @@
// Returns true if the path already exists on disk.
Import._fileExists = async function (filePath) {
try { await fs.stat(filePath); return true; } catch (_) { return false; }
try { await _stat(filePath); return true; } catch (_) { return false; }
};
// Write ArrayBuffer to disk via fs.writeFile.
@ -47,7 +62,7 @@
// previous import) we treat that as success: the bytes are already there.
Import._writeBuffer = async function (destPath, arrayBuffer) {
try {
await fs.writeFile(destPath, arrayBuffer);
await _writeFile(destPath, arrayBuffer);
} catch (e) {
const busy = e.code === 'EBUSY' || /resource busy/i.test(String(e.message));
if (!busy) throw e;
@ -179,7 +194,7 @@
const filename = meta.filename || path.basename(nativePath);
const contentType = _contentType(filename);
const buf = await fs.readFile(nativePath);
const buf = await _readFile(nativePath);
const size = buf.byteLength != null ? buf.byteLength : buf.length;
if (size <= SIMPLE_MAX) {

View file

@ -1,6 +1,6 @@
// app.jsx - main shell
const ACCENT = '#5B7CFA';
const ACCENT = '#E8821C';
function App() {
const [route, setRoute] = React.useState('home');
@ -23,6 +23,18 @@ function App() {
try { localStorage.setItem('df.sidebar.collapsed', next ? '1' : '0'); } catch {}
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(() => {

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.
// Each entry carries a `ccx` URL; the older `zxp`/`installer` fields are gone.
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',
ccx: '/downloads/dragonflight-mam-2.2.2.ccx',
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.',
latest: true,
latest: false,
},
];
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 = {
PROJECTS: [],
ASSETS: [],

View file

@ -153,7 +153,7 @@ function NewRecorderModal({ open, onClose }) {
const [recCodec, setRecCodec] = React.useState('hevc_nvenc');
// Custom target bitrate in Mbps for bitrate-controlled codecs (NVENC / x264 /
// 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
// MOV (fragmented, growing-capable); H.264 MP4.
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 [proxyOn, setProxyOn] = React.useState(true);
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 [submitting, setSubmitting] = React.useState(false);
const [submitErr, setSubmitErr] = React.useState(null);
@ -214,8 +220,10 @@ function NewRecorderModal({ open, onClose }) {
recording_framerate: '', // empty = match source
recording_resolution: 'native',
};
// Custom bitrate only applies to bitrate-controlled codecs (ProRes ignores it).
if (codecUsesBitrate && recBitrate) {
// Custom bitrate applies to bitrate-controlled codecs AND to growing-files
// 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`;
}
@ -224,7 +232,11 @@ function NewRecorderModal({ open, onClose }) {
} else if (sourceType === 'RTMP') {
body.source_config = { url: rtmpUrl };
} 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.node_id = dcNodeId || undefined;
} else {
@ -397,13 +409,37 @@ function NewRecorderModal({ open, onClose }) {
</div>
<div className="modal-section-body">
{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 className="field">
<label className="field-label">Video codec</label>
<select className="field-input" value={recCodec} onChange={e => setRecCodec(e.target.value)} style={{ appearance: 'auto' }}>
<option value="hevc_nvenc">All-Intra HEVC (NVENC) GPU, growing</option>
<option value="h264_nvenc">H.264 (NVENC) GPU</option>
<option value="prores_hq">ProRes 422 HQ 4:2:2 CPU</option>
<label className="field-label">
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="h264_nvenc">H.264 (NVENC, GPU)</option>
<option value="prores_hq">ProRes 422 HQ (4:2:2, CPU)</option>
<option value="prores">ProRes 422</option>
<option value="prores_lt">ProRes 422 LT</option>
<option value="prores_proxy">ProRes 422 Proxy</option>
@ -412,7 +448,7 @@ function NewRecorderModal({ open, onClose }) {
<option value="libx265">H.265 (x265, CPU)</option>
</select>
</div>
{codecUsesBitrate ? (
{showBitrate ? (
<div className="field">
<label className="field-label">Target bitrate (Mbps)</label>
<input
@ -425,8 +461,8 @@ function NewRecorderModal({ open, onClose }) {
) : (
<Field label="Bitrate" value="Quality-based (profile)" select />
)}
<Field label="Resolution" value="Auto — from source" select />
<Field label="Framerate" value="Auto — from source" select />
<Field label="Resolution" value="Auto (from source)" select />
<Field label="Framerate" value="Auto (from source)" select />
{/* #3: warn when the configured bitrate exceeds the probed source
bitrate re-encoding above source adds storage, not quality. */}
{codecUsesBitrate && (() => {
@ -444,6 +480,7 @@ function NewRecorderModal({ open, onClose }) {
return null;
})()}
</div>
</>
)}
{recTab === 'audio' && (
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
@ -455,8 +492,10 @@ function NewRecorderModal({ open, onClose }) {
)}
{recTab === 'container' && (
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
<Field label="Container" value={recContainer === 'mp4' ? 'MP4 (auto)' : 'Fragmented MOV (auto)'} select />
<Field label="Growing-file" value={recContainer === 'mov' ? 'Supported (edit-while-record)' : 'No'} select />
<Field label="Container"
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>
@ -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.
Requires the SMB share to be configured in Settings Storage.
</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>

View file

@ -11,6 +11,12 @@ function _normalizeNode(n, x, y) {
index: g.index ?? 0,
device: g.device || null,
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
@ -21,6 +27,13 @@ function _normalizeNode(n, x, y) {
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 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
gpus,
bmdPorts,
deltacastPorts,
// Legacy flat arrays kept for the stat-row summary cards
gpuCount: gpus.length,
bmdCount: bmdPorts.length,
@ -116,6 +130,7 @@ function Users() {
const [editingUser, setEditingUser] = React.useState(null);
const [resetUser, setResetUser] = React.useState(null);
const [menuFor, setMenuFor] = React.useState(null); // row id whose menu is open
const [confirm, confirmModal] = window.useConfirm();
const refreshUsers = React.useCallback(() => {
window.ZAMPP_API.fetch('/users')
@ -161,9 +176,9 @@ function Users() {
const onCreated = () => { refreshUsers(); setShowInvite(false); };
const deleteUser = (u) => {
const deleteUser = async (u) => {
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' })
.then(refreshUsers)
.catch(e => alert('Delete failed: ' + e.message));
@ -180,6 +195,7 @@ function Users() {
return (
<div className="page">
{confirmModal}
<div className="page-header">
<h1>Users &amp; Groups</h1>
<div className="spacer" />
@ -258,28 +274,7 @@ function Users() {
{tab === 'groups' && <GroupsPanel groups={groups} users={users} onChange={refreshGroups} />}
{tab === 'policies' && (
<div className="panel" style={{ padding: '32px 24px', color: 'var(--text-2)' }}>
<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>
<PoliciesPanel users={users} onChange={refreshUsers} />
)}
</div>
{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 }) {
const [name, setName] = React.useState(user.display_name || user.name || '');
const [saving, setSaving] = React.useState(false);
@ -424,6 +619,7 @@ function GroupsPanel({ groups, users, onChange }) {
const [newDesc, setNewDesc] = React.useState('');
const [expandedId, setExpandedId] = React.useState(null);
const [members, setMembers] = React.useState({}); // groupId -> [user]
const [confirm, confirmModal] = window.useConfirm();
const createGroup = () => {
if (!newName.trim()) return;
@ -432,8 +628,8 @@ function GroupsPanel({ groups, users, onChange }) {
.catch(e => alert('Create failed: ' + e.message));
};
const deleteGroup = (g) => {
if (!confirm(`Delete group "${g.name}"?`)) return;
const deleteGroup = async (g) => {
if (!(await confirm({ title: 'Delete group?', message: `Delete group "${g.name}"?` }))) return;
window.ZAMPP_API.fetch('/groups/' + g.id, { method: 'DELETE' })
.then(onChange)
.catch(e => alert('Delete failed: ' + e.message));
@ -468,6 +664,7 @@ function GroupsPanel({ groups, users, onChange }) {
return (
<div>
{confirmModal}
<div style={{ display: 'flex', alignItems: 'center', marginBottom: 12 }}>
<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.
@ -830,6 +1027,7 @@ function Containers() {
const [containers, setContainers] = React.useState(null);
const [restartFlashState, setRestartFlashState] = React.useState(null);
const [logsModalState, setLogsModalState] = React.useState(null);
const [confirm, confirmModal] = window.useConfirm();
// #111 - guard restart-flash timers against unmount.
const mountedRef = React.useRef(true);
const flashTimerRef = React.useRef(null);
@ -860,8 +1058,8 @@ function Containers() {
const showLogs = (c) => setLogsModal(c);
const restartContainer = (c) => {
if (!window.confirm('Restart container "' + c.name + '"?\nIn-flight requests will be dropped.')) return;
const restartContainer = async (c) => {
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' });
window.ZAMPP_API.fetch('/cluster/containers/' + encodeURIComponent(c.id || c.name) + '/restart', { method: 'POST' })
.then(() => {
@ -878,6 +1076,7 @@ function Containers() {
return (
<div className="page">
{confirmModal}
<div className="page-header">
<h1>Containers</h1>
<span className="subtitle">Docker Compose services across the cluster</span>
@ -976,7 +1175,11 @@ function Containers() {
<span>{(c.cpu || 0).toFixed(1)}%</span>
</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 style={{ display: "flex", gap: 4 }}>
<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.
// 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);
// Map of "node_id:portIndex" signal entry from /cluster/devices/blackmagic/signal
const [portSignals, setPortSignals] = React.useState({});
const [confirm, confirmModal] = window.useConfirm();
const refresh = React.useCallback(() => {
window.ZAMPP_API.fetch('/cluster')
@ -1195,8 +1541,8 @@ function Cluster() {
commands: [`ssh ${node.ip || node.id} 'docker stop node-agent'`],
});
const removeNode = (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;
const removeNode = async (node) => {
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' })
.then(() => refresh())
.catch(e => setAdviceModal({ title: 'Remove failed', lines: [e.message] }));
@ -1210,6 +1556,7 @@ function Cluster() {
return (
<div className="page">
{confirmModal}
<div className="page-header">
<h1>Cluster</h1>
<span className="subtitle">{NODES.filter(n => n.status === "online").length} of {NODES.length} nodes online</span>
@ -1365,6 +1712,23 @@ function Cluster() {
</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>
<span style={{
fontSize: 10, fontWeight: 600, padding: "2px 6px", borderRadius: 3,
@ -1379,6 +1743,9 @@ function Cluster() {
{/* ── Capture cards ── */}
<BmdCardPanel sel={sel} portSignals={portSignals} />
{/* ── Capture Drivers / SDKs ── */}
<DriverPanel sel={sel} />
<div style={{ display: "flex", gap: 6, marginTop: 6 }}>
<button className="btn ghost sm" onClick={() => nodeLogsHint(sel)}>Logs</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,
// mints a one-time auth token via /auth/tokens, and renders a ready-to-paste
// AddNodeModal Approach A onboarding wizard. Collects a node name, mints a
// 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.
//
// Role compose PROFILES mapping (see docker-compose.worker.yml):
// Worker "worker"
// Capture "worker capture"
// 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' },
];
// No role picker: the new node self-detects its hardware (GPU / DeckLink /
// Deltacast) in onboard-node.sh and auto-enables the matching compose profiles
// (worker always; + gpu / + capture when present). Zero manual choice.
function AddNodeModal({ onClose }) {
const [nodeName, setNodeName] = React.useState('');
const [role, setRole] = React.useState('worker');
const [apiUrl, setApiUrl] = React.useState('');
const [info, setInfo] = React.useState(null); // { scriptUrl, branch }
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
}, []);
const roleDef = ADD_NODE_ROLES.find(r => r.id === role) || ADD_NODE_ROLES[0];
const generate = async () => {
setError(null);
if (!nodeName.trim()) { setError('Node name is required.'); return; }
@ -1477,8 +1834,7 @@ function AddNodeModal({ onClose }) {
const scriptUrl = (info && info.scriptUrl)
|| 'https://forge.wilddragon.net/zgaetano/wild-dragon/raw/branch/main/deploy/onboard-node.sh';
const cmd =
`curl -sL ${scriptUrl} | NODE_TOKEN=${token} MAM_API_URL=${apiUrl.trim()} ` +
`NODE_ROLE=${role} PROFILES="${roleDef.profiles}" bash`;
`curl -sL ${scriptUrl} | NODE_TOKEN=${token} MAM_API_URL=${apiUrl.trim()} bash`;
setCommand(cmd);
} catch (e) {
setError(e.message || 'Network error');
@ -1511,21 +1867,6 @@ function AddNodeModal({ onClose }) {
value={nodeName} onChange={e => setNodeName(e.target.value)} />
</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 }}>
<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 }}
@ -1542,10 +1883,13 @@ function AddNodeModal({ onClose }) {
<React.Fragment>
<div style={{ marginBottom: 10, padding: 10, border: '1px solid var(--accent)', background: 'var(--accent-soft)', borderRadius: 6 }}>
<div style={{ fontSize: 11, fontWeight: 600, color: 'var(--accent-text)' }}>
This token is shown only once copy the command now.
This token is shown only once. Copy the command now.
</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>
<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 }}>
<li>SSH into the fresh Ubuntu machine.</li>
<li>Paste and run this command.</li>
@ -1716,7 +2060,7 @@ function TotpSection() {
</div>
<div>
<div style={{ fontSize: 12, color: 'var(--text-2)', marginBottom: 8 }}>
Scan the QR with Google Authenticator, Authy, or 1Password or enter this secret manually:
Scan the QR with Google Authenticator, Authy, or 1Password, or enter this secret manually:
</div>
<div className="mono" style={{ fontSize: 12, background: 'var(--bg-2)', padding: '6px 10px', borderRadius: 5, wordBreak: 'break-all', marginBottom: 12 }}>{enroll.secret}</div>
<div className="field">
@ -1920,7 +2264,7 @@ function StorageWarningBanner() {
}}>
<Icon name="alert" size={20} style={{ color: 'var(--danger)', flexShrink: 0, marginTop: 1 }} />
<div style={{ fontSize: 13, fontWeight: 700, letterSpacing: '0.02em', lineHeight: 1.5, color: 'var(--text-1)' }}>
WARNING THESE SETTINGS ARE MEANT TO BE SET ONCE AT INITIAL DEPLOYMENT.
WARNING: THESE SETTINGS ARE MEANT TO BE SET ONCE AT INITIAL DEPLOYMENT.
CHANGING STORAGE PATHS MID-CYCLE CAN CAUSE DATABASE CORRUPTION AND FILE LOSS.
PLEASE USE WITH CAUTION.
</div>
@ -2426,6 +2770,7 @@ function SdkVendorRow({ vendor, status, onDone }) {
const fileRef = React.useRef(null);
const [uploading, setUploading] = React.useState(false);
const [progress, setProgress] = React.useState(0);
const [confirm, confirmModal] = window.useConfirm();
const deployed = status && status.file_count > 0;
const lastUpload = status?.uploaded_at
@ -2467,8 +2812,8 @@ function SdkVendorRow({ vendor, status, onDone }) {
});
};
const clear = () => {
if (!confirm('Remove staged ' + vendor.name + ' SDK files?')) return;
const clear = async () => {
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' })
.then(() => onDone(vendor.name + ': cleared.', true))
.catch(e => onDone(vendor.name + ': ' + e.message, false));
@ -2476,6 +2821,7 @@ function SdkVendorRow({ vendor, status, onDone }) {
return (
<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 }}>
<strong style={{ fontSize: 13 }}>{vendor.name}</strong>
{deployed

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