diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000..a430681 --- /dev/null +++ b/.claude/launch.json @@ -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}]} diff --git a/.env.example b/.env.example index 493f47f..05b20c3 100644 --- a/.env.example +++ b/.env.example @@ -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. diff --git a/.gitignore b/.gitignore index ee1cfd5..34dce83 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/deploy/install-driver.sh b/deploy/install-driver.sh new file mode 100644 index 0000000..5d46aa2 --- /dev/null +++ b/deploy/install-driver.sh @@ -0,0 +1,325 @@ +#!/usr/bin/env bash +# ============================================================================ +# install-driver.sh +# ---------------------------------------------------------------------------- +# 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// (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// +# 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 " >&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 diff --git a/deploy/onboard-node.sh b/deploy/onboard-node.sh index 5208c4d..4ea5aa6 100644 --- a/deploy/onboard-node.sh +++ b/deploy/onboard-node.sh @@ -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" diff --git a/deploy/udev/99-wild-dragon-deltacast.rules b/deploy/udev/99-wild-dragon-deltacast.rules new file mode 100644 index 0000000..76b14b0 --- /dev/null +++ b/deploy/udev/99-wild-dragon-deltacast.rules @@ -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'" diff --git a/docker-compose.gpu.yml b/docker-compose.gpu.yml index e26eeb9..59e48e7 100644 --- a/docker-compose.gpu.yml +++ b/docker-compose.gpu.yml @@ -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 diff --git a/docker-compose.worker.yml b/docker-compose.worker.yml index 110ccd0..a5e6980 100644 --- a/docker-compose.worker.yml +++ b/docker-compose.worker.yml @@ -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// 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// + 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 diff --git a/docker-compose.yml b/docker-compose.yml index 3e66986..66f41b7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/docs/superpowers/plans/2026-06-01-deltacast-sdi-capture-plan.md b/docs/superpowers/plans/2026-06-01-deltacast-sdi-capture-plan.md new file mode 100644 index 0000000..87cb6fb --- /dev/null +++ b/docs/superpowers/plans/2026-06-01-deltacast-sdi-capture-plan.md @@ -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 ` 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 --port --audio-pipe + * [--signal-timeout ] + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 160–191) + +- [ ] **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 ~50–200KB. + +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 diff --git a/docs/superpowers/specs/2026-06-01-deltacast-sdi-capture-design.md b/docs/superpowers/specs/2026-06-01-deltacast-sdi-capture-design.md new file mode 100644 index 0000000..619b519 --- /dev/null +++ b/docs/superpowers/specs/2026-06-01-deltacast-sdi-capture-design.md @@ -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 ` (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 │ +│ -i /tmp/dc-audio-{sessionId} │ +│ │ +│ │ +└─────────────────────────────────────────────────────────┘ +``` + +### 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 Board index (0-based) + --port RX port index (0-based) + --audio-pipe Named FIFO path for PCM audio output + [--signal-timeout ] + [--audio-groups ] 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`. 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 1–4). + +--- + +## `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 diff --git a/sdk/README.md b/sdk/README.md new file mode 100644 index 0000000..af70c59 --- /dev/null +++ b/sdk/README.md @@ -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 `](../deploy/install-driver.sh), which reads +the vendor files from `sdk//`. 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//` 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. diff --git a/sdk/aja/.gitkeep b/sdk/aja/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/sdk/aja/README.md b/sdk/aja/README.md new file mode 100644 index 0000000..dac93d1 --- /dev/null +++ b/sdk/aja/README.md @@ -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). diff --git a/sdk/blackmagic/.gitkeep b/sdk/blackmagic/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/sdk/blackmagic/README.md b/sdk/blackmagic/README.md new file mode 100644 index 0000000..eafc5aa --- /dev/null +++ b/sdk/blackmagic/README.md @@ -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//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. diff --git a/sdk/deltacast/.gitkeep b/sdk/deltacast/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/sdk/deltacast/README.md b/sdk/deltacast/README.md new file mode 100644 index 0000000..ec81c6a --- /dev/null +++ b/sdk/deltacast/README.md @@ -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* (). 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 ` 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. diff --git a/sdk/ndi/.gitkeep b/sdk/ndi/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/sdk/ndi/README.md b/sdk/ndi/README.md new file mode 100644 index 0000000..4907199 --- /dev/null +++ b/sdk/ndi/README.md @@ -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.` 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. diff --git a/services/capture/.dockerignore b/services/capture/.dockerignore new file mode 100644 index 0000000..9e6950c --- /dev/null +++ b/services/capture/.dockerignore @@ -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 diff --git a/services/capture/Dockerfile b/services/capture/Dockerfile index 455dc2c..1730c6c 100644 --- a/services/capture/Dockerfile +++ b/services/capture/Dockerfile @@ -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) diff --git a/services/capture/deltacast-bridge/CMakeLists.txt b/services/capture/deltacast-bridge/CMakeLists.txt new file mode 100644 index 0000000..a877608 --- /dev/null +++ b/services/capture/deltacast-bridge/CMakeLists.txt @@ -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 + $ + $/deltacast-capture + COMMENT "Creating deltacast-capture compat symlink" +) diff --git a/services/capture/deltacast-bridge/main.c b/services/capture/deltacast-bridge/main.c new file mode 100644 index 0000000..ce41763 --- /dev/null +++ b/services/capture/deltacast-bridge/main.c @@ -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 --ports + * [--video-pipe-dir /dev/shm/deltacast] + * [--audio-pipe-dir /dev/shm/deltacast] + * [--signal-timeout ] + * + * Compat alias: --port treated as --ports (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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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; +} diff --git a/services/capture/sdk/DeckLinkAPI.h b/services/capture/sdk/DeckLinkAPI.h new file mode 100644 index 0000000..39068ad --- /dev/null +++ b/services/capture/sdk/DeckLinkAPI.h @@ -0,0 +1,1692 @@ +/* -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- + */ + + +/* + * -- AUTOMATICALLY GENERATED - DO NOT EDIT --- + */ + +#ifndef BMD_DECKLINKAPI_H +#define BMD_DECKLINKAPI_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 + +/* DeckLink API */ + +#include +#include "LinuxCOM.h" + +#include "DeckLinkAPITypes.h" +#include "DeckLinkAPIModes.h" +#include "DeckLinkAPIDiscovery.h" +#include "DeckLinkAPIConfiguration.h" +#include "DeckLinkAPIDeckControl.h" + +#define BLACKMAGIC_DECKLINK_API_MAGIC 1 + +// Type Declarations + + +// Interface ID Declarations + +BMD_CONST REFIID IID_IDeckLinkVideoOutputCallback = /* 5BE6DF26-02CE-433E-99D9-9A87C3AC171F */ { 0x5B,0xE6,0xDF,0x26,0x02,0xCE,0x43,0x3E,0x99,0xD9,0x9A,0x87,0xC3,0xAC,0x17,0x1F }; +BMD_CONST REFIID IID_IDeckLinkInputCallback = /* 3A94F075-C37D-4BA8-BCC0-1D778C8F881B */ { 0x3A,0x94,0xF0,0x75,0xC3,0x7D,0x4B,0xA8,0xBC,0xC0,0x1D,0x77,0x8C,0x8F,0x88,0x1B }; +BMD_CONST REFIID IID_IDeckLinkEncoderInputCallback = /* ACF13E61-F4A0-4974-A6A7-59AFF6268B31 */ { 0xAC,0xF1,0x3E,0x61,0xF4,0xA0,0x49,0x74,0xA6,0xA7,0x59,0xAF,0xF6,0x26,0x8B,0x31 }; +BMD_CONST REFIID IID_IDeckLinkVideoBufferAllocator = /* F35DFA8D-9078-4622-95BB-56894054EB0F */ { 0xF3,0x5D,0xFA,0x8D,0x90,0x78,0x46,0x22,0x95,0xBB,0x56,0x89,0x40,0x54,0xEB,0x0F }; +BMD_CONST REFIID IID_IDeckLinkVideoBufferAllocatorProvider = /* 6DF6F20A-D8DF-45D2-8914-383CE7E6243F */ { 0x6D,0xF6,0xF2,0x0A,0xD8,0xDF,0x45,0xD2,0x89,0x14,0x38,0x3C,0xE7,0xE6,0x24,0x3F }; +BMD_CONST REFIID IID_IDeckLinkAudioOutputCallback = /* 403C681B-7F46-4A12-B993-2BB127084EE6 */ { 0x40,0x3C,0x68,0x1B,0x7F,0x46,0x4A,0x12,0xB9,0x93,0x2B,0xB1,0x27,0x08,0x4E,0xE6 }; +BMD_CONST REFIID IID_IDeckLinkIterator = /* 50FB36CD-3063-4B73-BDBB-958087F2D8BA */ { 0x50,0xFB,0x36,0xCD,0x30,0x63,0x4B,0x73,0xBD,0xBB,0x95,0x80,0x87,0xF2,0xD8,0xBA }; +BMD_CONST REFIID IID_IDeckLinkAPIInformation = /* 7BEA3C68-730D-4322-AF34-8A7152B532A4 */ { 0x7B,0xEA,0x3C,0x68,0x73,0x0D,0x43,0x22,0xAF,0x34,0x8A,0x71,0x52,0xB5,0x32,0xA4 }; +BMD_CONST REFIID IID_IDeckLinkIPFlowAttributes = /* CDA938DA-6479-40C6-B2EC-A3579B3AEECD */ { 0xCD,0xA9,0x38,0xDA,0x64,0x79,0x40,0xC6,0xB2,0xEC,0xA3,0x57,0x9B,0x3A,0xEE,0xCD }; +BMD_CONST REFIID IID_IDeckLinkIPFlowStatus = /* 31C41656-4992-4396-BBE9-5F8406AAB5AF */ { 0x31,0xC4,0x16,0x56,0x49,0x92,0x43,0x96,0xBB,0xE9,0x5F,0x84,0x06,0xAA,0xB5,0xAF }; +BMD_CONST REFIID IID_IDeckLinkIPFlowSetting = /* 86DD9174-27D3-4032-B2AD-6067C3BB2424 */ { 0x86,0xDD,0x91,0x74,0x27,0xD3,0x40,0x32,0xB2,0xAD,0x60,0x67,0xC3,0xBB,0x24,0x24 }; +BMD_CONST REFIID IID_IDeckLinkIPFlow = /* C5FC83C7-5B8E-42A7-9A40-7C065955D4E1 */ { 0xC5,0xFC,0x83,0xC7,0x5B,0x8E,0x42,0xA7,0x9A,0x40,0x7C,0x06,0x59,0x55,0xD4,0xE1 }; +BMD_CONST REFIID IID_IDeckLinkIPFlowIterator = /* BD296AB2-A5C5-4153-888F-AAB1FDBD8A5C */ { 0xBD,0x29,0x6A,0xB2,0xA5,0xC5,0x41,0x53,0x88,0x8F,0xAA,0xB1,0xFD,0xBD,0x8A,0x5C }; +BMD_CONST REFIID IID_IDeckLinkOutput = /* 5F227C95-39D7-46C7-8B7D-9C81795FBBE4 */ { 0x5F,0x22,0x7C,0x95,0x39,0xD7,0x46,0xC7,0x8B,0x7D,0x9C,0x81,0x79,0x5F,0xBB,0xE4 }; +BMD_CONST REFIID IID_IDeckLinkInput = /* 6A515F8A-FBCE-4853-B0F7-2A09DB1ECA0B */ { 0x6A,0x51,0x5F,0x8A,0xFB,0xCE,0x48,0x53,0xB0,0xF7,0x2A,0x09,0xDB,0x1E,0xCA,0x0B }; +BMD_CONST REFIID IID_IDeckLinkIPExtensions = /* 46CF7903-A9FD-4D0B-8FFC-0103722AB442 */ { 0x46,0xCF,0x79,0x03,0xA9,0xFD,0x4D,0x0B,0x8F,0xFC,0x01,0x03,0x72,0x2A,0xB4,0x42 }; +BMD_CONST REFIID IID_IDeckLinkHDMIInputEDID = /* ABBBACBC-45BC-4665-9D92-ACE6E5A97902 */ { 0xAB,0xBB,0xAC,0xBC,0x45,0xBC,0x46,0x65,0x9D,0x92,0xAC,0xE6,0xE5,0xA9,0x79,0x02 }; +BMD_CONST REFIID IID_IDeckLinkEncoderInput = /* 46C1332E-6FD9-472A-8591-FE59C22192E1 */ { 0x46,0xC1,0x33,0x2E,0x6F,0xD9,0x47,0x2A,0x85,0x91,0xFE,0x59,0xC2,0x21,0x92,0xE1 }; +BMD_CONST REFIID IID_IDeckLinkVideoBuffer = /* 81F03D70-DE13-4B17-873A-C8AC9689C682 */ { 0x81,0xF0,0x3D,0x70,0xDE,0x13,0x4B,0x17,0x87,0x3A,0xC8,0xAC,0x96,0x89,0xC6,0x82 }; +BMD_CONST REFIID IID_IDeckLinkVideoFrame = /* 6502091C-615F-4F51-BAF6-45C4256DD5B0 */ { 0x65,0x02,0x09,0x1C,0x61,0x5F,0x4F,0x51,0xBA,0xF6,0x45,0xC4,0x25,0x6D,0xD5,0xB0 }; +BMD_CONST REFIID IID_IDeckLinkMutableVideoFrame = /* CF9EB134-0374-4C5B-95FA-1EC14819FF62 */ { 0xCF,0x9E,0xB1,0x34,0x03,0x74,0x4C,0x5B,0x95,0xFA,0x1E,0xC1,0x48,0x19,0xFF,0x62 }; +BMD_CONST REFIID IID_IDeckLinkVideoFrame3DExtensions = /* D4DBE9C6-B4D2-49D3-ABF2-B4E86C7391B0 */ { 0xD4,0xDB,0xE9,0xC6,0xB4,0xD2,0x49,0xD3,0xAB,0xF2,0xB4,0xE8,0x6C,0x73,0x91,0xB0 }; +BMD_CONST REFIID IID_IDeckLinkVideoFrameMetadataExtensions = /* E232A5B7-4DB4-44C9-9152-F47C12E5F051 */ { 0xE2,0x32,0xA5,0xB7,0x4D,0xB4,0x44,0xC9,0x91,0x52,0xF4,0x7C,0x12,0xE5,0xF0,0x51 }; +BMD_CONST REFIID IID_IDeckLinkVideoFrameMutableMetadataExtensions = /* CC198FC6-8298-4419-942D-8357EC355E58 */ { 0xCC,0x19,0x8F,0xC6,0x82,0x98,0x44,0x19,0x94,0x2D,0x83,0x57,0xEC,0x35,0x5E,0x58 }; +BMD_CONST REFIID IID_IDeckLinkVideoInputFrame = /* C9ADD3D2-BE52-488D-AB2D-7FDEF7AF0C95 */ { 0xC9,0xAD,0xD3,0xD2,0xBE,0x52,0x48,0x8D,0xAB,0x2D,0x7F,0xDE,0xF7,0xAF,0x0C,0x95 }; +BMD_CONST REFIID IID_IDeckLinkAncillaryPacket = /* F5C0D498-5CD3-4C77-9773-8EFA20BB334B */ { 0xF5,0xC0,0xD4,0x98,0x5C,0xD3,0x4C,0x77,0x97,0x73,0x8E,0xFA,0x20,0xBB,0x33,0x4B }; +BMD_CONST REFIID IID_IDeckLinkAncillaryPacketIterator = /* 10F1AA88-54BE-42F7-B9F8-EC2F5F099551 */ { 0x10,0xF1,0xAA,0x88,0x54,0xBE,0x42,0xF7,0xB9,0xF8,0xEC,0x2F,0x5F,0x09,0x95,0x51 }; +BMD_CONST REFIID IID_IDeckLinkVideoFrameAncillaryPackets = /* 8A72D630-8070-4D05-8A93-E60C40EE088A */ { 0x8A,0x72,0xD6,0x30,0x80,0x70,0x4D,0x05,0x8A,0x93,0xE6,0x0C,0x40,0xEE,0x08,0x8A }; +BMD_CONST REFIID IID_IDeckLinkVideoFrameAncillary = /* 732E723C-D1A4-4E29-9E8E-4A88797A0004 */ { 0x73,0x2E,0x72,0x3C,0xD1,0xA4,0x4E,0x29,0x9E,0x8E,0x4A,0x88,0x79,0x7A,0x00,0x04 }; +BMD_CONST REFIID IID_IDeckLinkEncoderPacket = /* B693F36C-316E-4AF1-B6C2-F389A4BCA620 */ { 0xB6,0x93,0xF3,0x6C,0x31,0x6E,0x4A,0xF1,0xB6,0xC2,0xF3,0x89,0xA4,0xBC,0xA6,0x20 }; +BMD_CONST REFIID IID_IDeckLinkEncoderVideoPacket = /* 4E7FD944-E8C7-4EAC-B8C0-7B77F80F5AE0 */ { 0x4E,0x7F,0xD9,0x44,0xE8,0xC7,0x4E,0xAC,0xB8,0xC0,0x7B,0x77,0xF8,0x0F,0x5A,0xE0 }; +BMD_CONST REFIID IID_IDeckLinkEncoderAudioPacket = /* 49E8EDC8-693B-4E14-8EF6-12C658F5A07A */ { 0x49,0xE8,0xED,0xC8,0x69,0x3B,0x4E,0x14,0x8E,0xF6,0x12,0xC6,0x58,0xF5,0xA0,0x7A }; +BMD_CONST REFIID IID_IDeckLinkH265NALPacket = /* 639C8E0B-68D5-4BDE-A6D4-95F3AEAFF2E7 */ { 0x63,0x9C,0x8E,0x0B,0x68,0xD5,0x4B,0xDE,0xA6,0xD4,0x95,0xF3,0xAE,0xAF,0xF2,0xE7 }; +BMD_CONST REFIID IID_IDeckLinkAudioInputPacket = /* E43D5870-2894-11DE-8C30-0800200C9A66 */ { 0xE4,0x3D,0x58,0x70,0x28,0x94,0x11,0xDE,0x8C,0x30,0x08,0x00,0x20,0x0C,0x9A,0x66 }; +BMD_CONST REFIID IID_IDeckLinkScreenPreviewCallback = /* D4FA2345-9FBA-4497-95C3-C0C3CED3CDA8 */ { 0xD4,0xFA,0x23,0x45,0x9F,0xBA,0x44,0x97,0x95,0xC3,0xC0,0xC3,0xCE,0xD3,0xCD,0xA8 }; +BMD_CONST REFIID IID_IDeckLinkGLScreenPreviewHelper = /* CEB778E2-C202-4EC8-9085-0CD285CC5522 */ { 0xCE,0xB7,0x78,0xE2,0xC2,0x02,0x4E,0xC8,0x90,0x85,0x0C,0xD2,0x85,0xCC,0x55,0x22 }; +BMD_CONST REFIID IID_IDeckLinkNotificationCallback = /* B002A1EC-070D-4288-8289-BD5D36E5FF0D */ { 0xB0,0x02,0xA1,0xEC,0x07,0x0D,0x42,0x88,0x82,0x89,0xBD,0x5D,0x36,0xE5,0xFF,0x0D }; +BMD_CONST REFIID IID_IDeckLinkNotification = /* 1D70FAAC-FD27-4866-9DE6-0939D1E4C7F1 */ { 0x1D,0x70,0xFA,0xAC,0xFD,0x27,0x48,0x66,0x9D,0xE6,0x09,0x39,0xD1,0xE4,0xC7,0xF1 }; +BMD_CONST REFIID IID_IDeckLinkProfileAttributes = /* F47551D7-AD22-47AF-BCFD-6BE88AA879D9 */ { 0xF4,0x75,0x51,0xD7,0xAD,0x22,0x47,0xAF,0xBC,0xFD,0x6B,0xE8,0x8A,0xA8,0x79,0xD9 }; +BMD_CONST REFIID IID_IDeckLinkProfileIterator = /* 29E5A8C0-8BE4-46EB-93AC-31DAAB5B7BF2 */ { 0x29,0xE5,0xA8,0xC0,0x8B,0xE4,0x46,0xEB,0x93,0xAC,0x31,0xDA,0xAB,0x5B,0x7B,0xF2 }; +BMD_CONST REFIID IID_IDeckLinkProfile = /* 16093466-674A-432B-9DA0-1AC2C5A8241C */ { 0x16,0x09,0x34,0x66,0x67,0x4A,0x43,0x2B,0x9D,0xA0,0x1A,0xC2,0xC5,0xA8,0x24,0x1C }; +BMD_CONST REFIID IID_IDeckLinkProfileCallback = /* A4F9341E-97AA-4E04-8935-15F809898CEA */ { 0xA4,0xF9,0x34,0x1E,0x97,0xAA,0x4E,0x04,0x89,0x35,0x15,0xF8,0x09,0x89,0x8C,0xEA }; +BMD_CONST REFIID IID_IDeckLinkProfileManager = /* 30D41429-3998-4B6D-84F8-78C94A797C6E */ { 0x30,0xD4,0x14,0x29,0x39,0x98,0x4B,0x6D,0x84,0xF8,0x78,0xC9,0x4A,0x79,0x7C,0x6E }; +BMD_CONST REFIID IID_IDeckLinkStatistics = /* 21CB2ED1-4429-42BE-AAF3-22A3B1DD3AE0 */ { 0x21,0xCB,0x2E,0xD1,0x44,0x29,0x42,0xBE,0xAA,0xF3,0x22,0xA3,0xB1,0xDD,0x3A,0xE0 }; +BMD_CONST REFIID IID_IDeckLinkStatus = /* 2A04A635-ED42-41EF-9342-0E11F8CF6B5E */ { 0x2A,0x04,0xA6,0x35,0xED,0x42,0x41,0xEF,0x93,0x42,0x0E,0x11,0xF8,0xCF,0x6B,0x5E }; +BMD_CONST REFIID IID_IDeckLinkKeyer = /* 89AFCAF5-65F8-421E-98F7-96FE5F5BFBA3 */ { 0x89,0xAF,0xCA,0xF5,0x65,0xF8,0x42,0x1E,0x98,0xF7,0x96,0xFE,0x5F,0x5B,0xFB,0xA3 }; +BMD_CONST REFIID IID_IDeckLinkVideoConversion = /* 94C536D6-C821-42F5-A600-C66629955101 */ { 0x94,0xC5,0x36,0xD6,0xC8,0x21,0x42,0xF5,0xA6,0x00,0xC6,0x66,0x29,0x95,0x51,0x01 }; +BMD_CONST REFIID IID_IDeckLinkDeviceNotificationCallback = /* 4997053B-0ADF-4CC8-AC70-7A50C4BE728F */ { 0x49,0x97,0x05,0x3B,0x0A,0xDF,0x4C,0xC8,0xAC,0x70,0x7A,0x50,0xC4,0xBE,0x72,0x8F }; +BMD_CONST REFIID IID_IDeckLinkDiscovery = /* CDBF631C-BC76-45FA-B44D-C55059BC6101 */ { 0xCD,0xBF,0x63,0x1C,0xBC,0x76,0x45,0xFA,0xB4,0x4D,0xC5,0x50,0x59,0xBC,0x61,0x01 }; + +/* Enum BMDBufferAccessFlags - Flags to describe access requirements to a video frame buffer */ + +typedef uint32_t BMDBufferAccessFlags; +enum _BMDBufferAccessFlags { + bmdBufferAccessReadAndWrite = 1 << 0 | 1 << 1, + bmdBufferAccessRead = 1 << 0, + bmdBufferAccessWrite = 1 << 1 +}; + +/* Enum BMDVideoOutputFlags - Flags to control the output of ancillary data along with video. */ + +typedef uint32_t BMDVideoOutputFlags; +enum _BMDVideoOutputFlags { + bmdVideoOutputFlagDefault = 0, + bmdVideoOutputVANC = 1 << 0, + bmdVideoOutputVITC = 1 << 1, + bmdVideoOutputRP188 = 1 << 2, + bmdVideoOutputDualStream3D = 1 << 4, + bmdVideoOutputSynchronizeToPlaybackGroup = 1 << 6, + bmdVideoOutputDolbyVision = 1 << 7 +}; + +/* Enum BMDSupportedVideoModeFlags - Flags to describe supported video modes */ + +typedef uint32_t BMDSupportedVideoModeFlags; +enum _BMDSupportedVideoModeFlags { + bmdSupportedVideoModeDefault = 0, + bmdSupportedVideoModeKeying = 1 << 0, + bmdSupportedVideoModeDualStream3D = 1 << 1, + bmdSupportedVideoModeSDISingleLink = 1 << 2, + bmdSupportedVideoModeSDIDualLink = 1 << 3, + bmdSupportedVideoModeSDIQuadLink = 1 << 4, + bmdSupportedVideoModeInAnyProfile = 1 << 5, + bmdSupportedVideoModePsF = 1 << 6, + bmdSupportedVideoModeDolbyVision = 1 << 7, + bmdSupportedVideoModeEthernetIP10 = 1 << 8 +}; + +/* Enum BMDPacketType - Type of packet */ + +typedef uint32_t BMDPacketType; +enum _BMDPacketType { + bmdPacketTypeStreamInterruptedMarker = /* 'sint' */ 0x73696E74, // A packet of this type marks the time when a video stream was interrupted, for example by a disconnected cable + bmdPacketTypeStreamData = /* 'sdat' */ 0x73646174 // Regular stream data +}; + +/* Enum BMDFrameFlags - Frame flags */ + +typedef uint32_t BMDFrameFlags; +enum _BMDFrameFlags { + bmdFrameFlagDefault = 0, + bmdFrameFlagFlipVertical = 1 << 0, + bmdFrameFlagMonitorOutOnly = 1 << 3, + bmdFrameContainsHDRMetadata = 1 << 1, + bmdFrameContainsDolbyVisionMetadata = 1 << 4, + + /* Flags that are applicable only to instances of IDeckLinkVideoInputFrame */ + + bmdFrameCapturedAsPsF = 1 << 30, + bmdFrameHasNoInputSource = 1 << 31 +}; + +/* Enum BMDVideoInputFlags - Flags applicable to video input */ + +typedef uint32_t BMDVideoInputFlags; +enum _BMDVideoInputFlags { + bmdVideoInputFlagDefault = 0, + bmdVideoInputEnableFormatDetection = 1 << 0, + bmdVideoInputDualStream3D = 1 << 1, + bmdVideoInputSynchronizeToCaptureGroup = 1 << 2 +}; + +/* Enum BMDVideoInputFormatChangedEvents - Bitmask passed to the VideoInputFormatChanged notification to identify the properties of the input signal that have changed */ + +typedef uint32_t BMDVideoInputFormatChangedEvents; +enum _BMDVideoInputFormatChangedEvents { + bmdVideoInputDisplayModeChanged = 1 << 0, + bmdVideoInputFieldDominanceChanged = 1 << 1, + bmdVideoInputColorspaceChanged = 1 << 2 +}; + +/* Enum BMDDetectedVideoInputFormatFlags - Flags passed to the VideoInputFormatChanged notification to describe the detected video input signal */ + +typedef uint32_t BMDDetectedVideoInputFormatFlags; +enum _BMDDetectedVideoInputFormatFlags { + bmdDetectedVideoInputYCbCr422 = 1 << 0, + bmdDetectedVideoInputRGB444 = 1 << 1, + bmdDetectedVideoInputDualStream3D = 1 << 2, + bmdDetectedVideoInput12BitDepth = 1 << 3, + bmdDetectedVideoInput10BitDepth = 1 << 4, + bmdDetectedVideoInput8BitDepth = 1 << 5 +}; + +/* Enum BMDDeckLinkCapturePassthroughMode - Enumerates whether the video output is electrically connected to the video input or if the clean switching mode is enabled */ + +typedef uint32_t BMDDeckLinkCapturePassthroughMode; +enum _BMDDeckLinkCapturePassthroughMode { + bmdDeckLinkCapturePassthroughModeDisabled = /* 'pdis' */ 0x70646973, + bmdDeckLinkCapturePassthroughModeDirect = /* 'pdir' */ 0x70646972, + bmdDeckLinkCapturePassthroughModeCleanSwitch = /* 'pcln' */ 0x70636C6E +}; + +/* Enum BMDOutputFrameCompletionResult - Frame Completion Callback */ + +typedef uint32_t BMDOutputFrameCompletionResult; +enum _BMDOutputFrameCompletionResult { + bmdOutputFrameCompleted, + bmdOutputFrameDisplayedLate, + bmdOutputFrameDropped, + bmdOutputFrameFlushed +}; + +/* Enum BMDReferenceStatus - GenLock input status */ + +typedef uint32_t BMDReferenceStatus; +enum _BMDReferenceStatus { + bmdReferenceUnlocked = 0, + bmdReferenceNotSupportedByHardware = 1 << 0, + bmdReferenceLocked = 1 << 1 +}; + +/* Enum BMDEthernetNMOSRegistryState - */ + +typedef uint32_t BMDEthernetNMOSRegistryState; +enum _BMDEthernetNMOSRegistryState { + bmdEthernetNMOSRegistryConnecting = /* 'conn' */ 0x636F6E6E, + bmdEthernetNMOSRegistryActive = /* 'good' */ 0x676F6F64, + bmdEthernetNMOSRegistryError = /* 'erro' */ 0x6572726F +}; + +/* Enum BMDAudioFormat - Audio Format */ + +typedef uint32_t BMDAudioFormat; +enum _BMDAudioFormat { + bmdAudioFormatPCM = /* 'lpcm' */ 0x6C70636D // Linear signed PCM samples +}; + +/* Enum BMDAudioSampleRate - Audio sample rates supported for output/input */ + +typedef uint32_t BMDAudioSampleRate; +enum _BMDAudioSampleRate { + bmdAudioSampleRate48kHz = 48000 +}; + +/* Enum BMDAudioSampleType - Audio sample sizes supported for output/input */ + +typedef uint32_t BMDAudioSampleType; +enum _BMDAudioSampleType { + bmdAudioSampleType16bitInteger = 16, + bmdAudioSampleType32bitInteger = 32 +}; + +/* Enum BMDAudioOutputStreamType - Audio output stream type */ + +typedef uint32_t BMDAudioOutputStreamType; +enum _BMDAudioOutputStreamType { + bmdAudioOutputStreamContinuous, + bmdAudioOutputStreamContinuousDontResample, + bmdAudioOutputStreamTimestamped +}; + +/* Enum BMDAncillaryPacketFormat - Ancillary packet format */ + +typedef uint32_t BMDAncillaryPacketFormat; +enum _BMDAncillaryPacketFormat { + bmdAncillaryPacketFormatUInt8 = /* 'ui08' */ 0x75693038, + bmdAncillaryPacketFormatUInt16 = /* 'ui16' */ 0x75693136, + bmdAncillaryPacketFormatYCbCr10 = /* 'v210' */ 0x76323130 +}; + +/* Enum BMDTimecodeFormat - Timecode formats for frame metadata */ + +typedef uint32_t BMDTimecodeFormat; +enum _BMDTimecodeFormat { + bmdTimecodeRP188VITC1 = /* 'rpv1' */ 0x72707631, // RP188 timecode where DBB1 equals VITC1 (line 9) + bmdTimecodeRP188VITC2 = /* 'rp12' */ 0x72703132, // RP188 timecode where DBB1 equals VITC2 (line 9 for progressive or line 571 for interlaced/PsF) + bmdTimecodeRP188LTC = /* 'rplt' */ 0x72706C74, // RP188 timecode where DBB1 equals LTC (line 10) + bmdTimecodeRP188HighFrameRate = /* 'rphr' */ 0x72706872, // RP188 timecode where DBB1 is an HFRTC (SMPTE ST 12-3), the only timecode allowing the frame value to go above 30 + bmdTimecodeRP188Any = /* 'rp18' */ 0x72703138, // Convenience for capture, returning the first valid timecode in {HFRTC (if supported), VITC1, VITC2, LTC } + bmdTimecodeVITC = /* 'vitc' */ 0x76697463, + bmdTimecodeVITCField2 = /* 'vit2' */ 0x76697432, + bmdTimecodeSerial = /* 'seri' */ 0x73657269 +}; + +/* Enum BMDAnalogVideoFlags - Analog video display flags */ + +typedef uint32_t BMDAnalogVideoFlags; +enum _BMDAnalogVideoFlags { + bmdAnalogVideoFlagCompositeSetup75 = 1 << 0, + bmdAnalogVideoFlagComponentBetacamLevels = 1 << 1 +}; + +/* Enum BMDAudioOutputAnalogAESSwitch - Audio output Analog/AESEBU switch */ + +typedef uint32_t BMDAudioOutputAnalogAESSwitch; +enum _BMDAudioOutputAnalogAESSwitch { + bmdAudioOutputSwitchAESEBU = /* 'aes ' */ 0x61657320, + bmdAudioOutputSwitchAnalog = /* 'anlg' */ 0x616E6C67 +}; + +/* Enum BMDVideoOutputConversionMode - Video/audio conversion mode */ + +typedef uint32_t BMDVideoOutputConversionMode; +enum _BMDVideoOutputConversionMode { + bmdNoVideoOutputConversion = /* 'none' */ 0x6E6F6E65, + bmdVideoOutputLetterboxDownconversion = /* 'ltbx' */ 0x6C746278, + bmdVideoOutputAnamorphicDownconversion = /* 'amph' */ 0x616D7068, + bmdVideoOutputHD720toHD1080Conversion = /* '720c' */ 0x37323063, + bmdVideoOutputHardwareLetterboxDownconversion = /* 'HWlb' */ 0x48576C62, + bmdVideoOutputHardwareAnamorphicDownconversion = /* 'HWam' */ 0x4857616D, + bmdVideoOutputHardwareCenterCutDownconversion = /* 'HWcc' */ 0x48576363, + bmdVideoOutputHardware720p1080pCrossconversion = /* 'xcap' */ 0x78636170, + bmdVideoOutputHardwareAnamorphic720pUpconversion = /* 'ua7p' */ 0x75613770, + bmdVideoOutputHardwareAnamorphic1080iUpconversion = /* 'ua1i' */ 0x75613169, + bmdVideoOutputHardwareAnamorphic149To720pUpconversion = /* 'u47p' */ 0x75343770, + bmdVideoOutputHardwareAnamorphic149To1080iUpconversion = /* 'u41i' */ 0x75343169, + bmdVideoOutputHardwarePillarbox720pUpconversion = /* 'up7p' */ 0x75703770, + bmdVideoOutputHardwarePillarbox1080iUpconversion = /* 'up1i' */ 0x75703169 +}; + +/* Enum BMDVideoInputConversionMode - Video input conversion mode */ + +typedef uint32_t BMDVideoInputConversionMode; +enum _BMDVideoInputConversionMode { + bmdNoVideoInputConversion = /* 'none' */ 0x6E6F6E65, + bmdVideoInputLetterboxDownconversionFromHD1080 = /* '10lb' */ 0x31306C62, + bmdVideoInputAnamorphicDownconversionFromHD1080 = /* '10am' */ 0x3130616D, + bmdVideoInputLetterboxDownconversionFromHD720 = /* '72lb' */ 0x37326C62, + bmdVideoInputAnamorphicDownconversionFromHD720 = /* '72am' */ 0x3732616D, + bmdVideoInputLetterboxUpconversion = /* 'lbup' */ 0x6C627570, + bmdVideoInputAnamorphicUpconversion = /* 'amup' */ 0x616D7570 +}; + +/* Enum BMDVideo3DPackingFormat - Video 3D packing format */ + +typedef uint32_t BMDVideo3DPackingFormat; +enum _BMDVideo3DPackingFormat { + bmdVideo3DPackingSidebySideHalf = /* 'sbsh' */ 0x73627368, + bmdVideo3DPackingLinebyLine = /* 'lbyl' */ 0x6C62796C, + bmdVideo3DPackingTopAndBottom = /* 'tabo' */ 0x7461626F, + bmdVideo3DPackingFramePacking = /* 'frpk' */ 0x6672706B, + bmdVideo3DPackingLeftOnly = /* 'left' */ 0x6C656674, + bmdVideo3DPackingRightOnly = /* 'righ' */ 0x72696768 +}; + +/* Enum BMDIdleVideoOutputOperation - Video output operation when not playing video */ + +typedef uint32_t BMDIdleVideoOutputOperation; +enum _BMDIdleVideoOutputOperation { + bmdIdleVideoOutputBlack = /* 'blac' */ 0x626C6163, + bmdIdleVideoOutputLastFrame = /* 'lafa' */ 0x6C616661 +}; + +/* Enum BMDVideoEncoderFrameCodingMode - Video frame coding mode */ + +typedef uint32_t BMDVideoEncoderFrameCodingMode; +enum _BMDVideoEncoderFrameCodingMode { + bmdVideoEncoderFrameCodingModeInter = /* 'inte' */ 0x696E7465, + bmdVideoEncoderFrameCodingModeIntra = /* 'intr' */ 0x696E7472 +}; + +/* Enum BMDDNxHRLevel - DNxHR Levels */ + +typedef uint32_t BMDDNxHRLevel; +enum _BMDDNxHRLevel { + bmdDNxHRLevelSQ = /* 'dnsq' */ 0x646E7371, + bmdDNxHRLevelLB = /* 'dnlb' */ 0x646E6C62, + bmdDNxHRLevelHQ = /* 'dnhq' */ 0x646E6871, + bmdDNxHRLevelHQX = /* 'dhqx' */ 0x64687178, + bmdDNxHRLevel444 = /* 'd444' */ 0x64343434 +}; + +/* Enum BMDLinkConfiguration - Video link configuration */ + +typedef uint32_t BMDLinkConfiguration; +enum _BMDLinkConfiguration { + bmdLinkConfigurationSingleLink = /* 'lcsl' */ 0x6C63736C, + bmdLinkConfigurationDualLink = /* 'lcdl' */ 0x6C63646C, + bmdLinkConfigurationQuadLink = /* 'lcql' */ 0x6C63716C +}; + +/* Enum BMDDeviceInterface - Device interface type */ + +typedef uint32_t BMDDeviceInterface; +enum _BMDDeviceInterface { + bmdDeviceInterfacePCI = /* 'pci ' */ 0x70636920, + bmdDeviceInterfaceUSB = /* 'usb ' */ 0x75736220, + bmdDeviceInterfaceThunderbolt = /* 'thun' */ 0x7468756E +}; + +/* Enum BMDColorspace - Colorspace */ + +typedef uint32_t BMDColorspace; +enum _BMDColorspace { + bmdColorspaceRec601 = /* 'r601' */ 0x72363031, + bmdColorspaceRec709 = /* 'r709' */ 0x72373039, + bmdColorspaceRec2020 = /* '2020' */ 0x32303230, + bmdColorspaceDolbyVisionNative = /* 'DoVi' */ 0x446F5669, // For bmdDeckLinkConfigVideoOutputConversionColorspaceDestination with 12-bit RGB + bmdColorspaceP3D65 = /* 'P3D6' */ 0x50334436, // For bmdDeckLinkConfigVideoOutputConversionColorspaceSource only + bmdColorspaceUnknown = /* 'Ncol' */ 0x4E636F6C // For disabling bmdDeckLinkConfigVideoOutputConversionColorspaceDestination +}; + +/* Enum BMDDynamicRange - SDR or HDR */ + +typedef uint32_t BMDDynamicRange; +enum _BMDDynamicRange { + bmdDynamicRangeSDR = 0, // Standard Dynamic Range in accordance with SMPTE ST 2036-1 + bmdDynamicRangeHDRStaticPQ = 1 << 29, // High Dynamic Range PQ in accordance with SMPTE ST 2084 + bmdDynamicRangeHDRStaticHLG = 1 << 30 // High Dynamic Range HLG in accordance with ITU-R BT.2100-0 +}; + +/* Enum BMDMezzanineType - */ + +typedef uint32_t BMDMezzanineType; +enum _BMDMezzanineType { + bmdMezzanineTypeNone = 0, // No mezzanine board + bmdMezzanineTypeHDMI14OpticalSDI = /* 'mza1' */ 0x6D7A6131, // Mezzanine board with HDMI 1.4 and Optical SDI + bmdMezzanineTypeQuadSDI = /* 'mz4s' */ 0x6D7A3473, // Mezzanine board with four SDI connectors + bmdMezzanineTypeHDMI20OpticalSDI = /* 'mza2' */ 0x6D7A6132, // Mezzanine board with HDMI 2.0 and Optical SDI + bmdMezzanineTypeHDMI21RS422 = /* 'mzhr' */ 0x6D7A6872 // Mezzanine boards with HDMI 2.1 and RS422 +}; + +/* Enum BMDDeckLinkHDMIInputEDIDID - DeckLink HDMI Input EDID ID */ + +typedef uint32_t BMDDeckLinkHDMIInputEDIDID; +enum _BMDDeckLinkHDMIInputEDIDID { + + /* Integers */ + + bmdDeckLinkHDMIInputEDIDDynamicRange = /* 'HIDy' */ 0x48494479 // Parameter is of type BMDDynamicRange. Default is (bmdDynamicRangeSDR|bmdDynamicRangeHDRStaticPQ) +}; + +/* Enum BMDDeckLinkFrameMetadataID - DeckLink Frame Metadata ID */ + +typedef uint32_t BMDDeckLinkFrameMetadataID; +enum _BMDDeckLinkFrameMetadataID { + + /* Integers */ + + bmdDeckLinkFrameMetadataColorspace = /* 'cspc' */ 0x63737063, // Colorspace of video frame (see BMDColorspace) + bmdDeckLinkFrameMetadataHDRElectroOpticalTransferFunc = /* 'eotf' */ 0x656F7466, // EOTF in range 0-7 as per CEA 861.3 + bmdDeckLinkFrameMetadataRTPTimestamp = /* 'rtpt' */ 0x72747074, // RTP timestamp + + /* Dolby Vision only - Bytes */ + + bmdDeckLinkFrameMetadataDolbyVision = /* 'dovi' */ 0x646F7669, // Dolby Vision Metadata + + /* CEA/SMPTE only - HDR Metadata Floats */ + + bmdDeckLinkFrameMetadataHDRDisplayPrimariesRedX = /* 'hdrx' */ 0x68647278, // Red display primaries in range 0.0 - 1.0 + bmdDeckLinkFrameMetadataHDRDisplayPrimariesRedY = /* 'hdry' */ 0x68647279, // Red display primaries in range 0.0 - 1.0 + bmdDeckLinkFrameMetadataHDRDisplayPrimariesGreenX = /* 'hdgx' */ 0x68646778, // Green display primaries in range 0.0 - 1.0 + bmdDeckLinkFrameMetadataHDRDisplayPrimariesGreenY = /* 'hdgy' */ 0x68646779, // Green display primaries in range 0.0 - 1.0 + bmdDeckLinkFrameMetadataHDRDisplayPrimariesBlueX = /* 'hdbx' */ 0x68646278, // Blue display primaries in range 0.0 - 1.0 + bmdDeckLinkFrameMetadataHDRDisplayPrimariesBlueY = /* 'hdby' */ 0x68646279, // Blue display primaries in range 0.0 - 1.0 + bmdDeckLinkFrameMetadataHDRWhitePointX = /* 'hdwx' */ 0x68647778, // White point in range 0.0 - 1.0 + bmdDeckLinkFrameMetadataHDRWhitePointY = /* 'hdwy' */ 0x68647779, // White point in range 0.0 - 1.0 + bmdDeckLinkFrameMetadataHDRMaxDisplayMasteringLuminance = /* 'hdml' */ 0x68646D6C, // Max display mastering luminance in range 1 cd/m2 - 65535 cd/m2 + bmdDeckLinkFrameMetadataHDRMinDisplayMasteringLuminance = /* 'hmil' */ 0x686D696C, // Min display mastering luminance in range 0.0001 cd/m2 - 6.5535 cd/m2 + bmdDeckLinkFrameMetadataHDRMaximumContentLightLevel = /* 'mcll' */ 0x6D636C6C, // Maximum Content Light Level in range 1 cd/m2 - 65535 cd/m2 + bmdDeckLinkFrameMetadataHDRMaximumFrameAverageLightLevel = /* 'fall' */ 0x66616C6C // Maximum Frame Average Light Level in range 1 cd/m2 - 65535 cd/m2 +}; + +/* Enum BMDEthernetLinkState - The state of the Ethernet link */ + +typedef uint32_t BMDEthernetLinkState; +enum _BMDEthernetLinkState { + bmdEthernetLinkStateDisconnected = /* 'elds' */ 0x656C6473, + bmdEthernetLinkStateConnectedUnbound = /* 'elcu' */ 0x656C6375, + bmdEthernetLinkStateConnectedBound = /* 'elcb' */ 0x656C6362 +}; + +/* Enum BMDProfileID - Identifies a profile */ + +typedef uint32_t BMDProfileID; +enum _BMDProfileID { + bmdProfileOneSubDeviceFullDuplex = /* '1dfd' */ 0x31646664, + bmdProfileOneSubDeviceHalfDuplex = /* '1dhd' */ 0x31646864, + bmdProfileTwoSubDevicesFullDuplex = /* '2dfd' */ 0x32646664, + bmdProfileTwoSubDevicesHalfDuplex = /* '2dhd' */ 0x32646864, + bmdProfileFourSubDevicesHalfDuplex = /* '4dhd' */ 0x34646864 +}; + +/* Enum BMDHDMITimecodePacking - Packing form of timecode on HDMI */ + +typedef uint32_t BMDHDMITimecodePacking; +enum _BMDHDMITimecodePacking { + bmdHDMITimecodePackingIEEEOUI000085 = 0x00008500, + bmdHDMITimecodePackingIEEEOUI080046 = 0x08004601, + bmdHDMITimecodePackingIEEEOUI5CF9F0 = 0x5CF9F003 +}; + +/* Enum BMDInternalKeyingAncillaryDataSource - Source for VANC and timecode data when performing internal keying */ + +typedef uint32_t BMDInternalKeyingAncillaryDataSource; +enum _BMDInternalKeyingAncillaryDataSource { + bmdInternalKeyingUsesAncillaryDataFromInputSignal = /* 'ikai' */ 0x696B6169, + bmdInternalKeyingUsesAncillaryDataFromKeyFrame = /* 'ikak' */ 0x696B616B +}; + +/* Enum BMDAudioOutputXLRDelayType - Audio output XLR delay types */ + +typedef uint32_t BMDAudioOutputXLRDelayType; +enum _BMDAudioOutputXLRDelayType { + bmdAudioOutputXLRDelayTypeTime = /* 'dtms' */ 0x64746D73, + bmdAudioOutputXLRDelayTypeFrames = /* 'dtfr' */ 0x64746672 +}; + +/* Enum BMDLanguage - Languages */ + +typedef uint32_t BMDLanguage; +enum _BMDLanguage { + bmdLanguageEnglish = /* 'enUS' */ 0x656E5553, + bmdLanguageSimplifiedChinese = /* 'zhCN' */ 0x7A68434E, + bmdLanguageJapanese = /* 'jaJP' */ 0x6A614A50, + bmdLanguageKorean = /* 'koKR' */ 0x6B6F4B52, + bmdLanguageSpanish = /* 'esES' */ 0x65734553, + bmdLanguageGerman = /* 'deDE' */ 0x64654445, + bmdLanguageFrench = /* 'frFR' */ 0x66724652, + bmdLanguageRussian = /* 'ruRU' */ 0x72755255, + bmdLanguageItalian = /* 'itIT' */ 0x69744954, + bmdLanguagePortuguese = /* 'ptBR' */ 0x70744252, + bmdLanguageTurkish = /* 'trTR' */ 0x74725452, + bmdLanguagePolish = /* 'plPL' */ 0x706C504C, + bmdLanguageUkrainian = /* 'ukUA' */ 0x756B5541 +}; + +/* Enum BMDAudioMeterType - Audio meter type */ + +typedef uint32_t BMDAudioMeterType; +enum _BMDAudioMeterType { + bmdAudioMeterTypeVUMinus18db = /* 'vu18' */ 0x76753138, + bmdAudioMeterTypeVUMinus20db = /* 'vu20' */ 0x76753230, + bmdAudioMeterTypePPMMinus18db = /* 'pm18' */ 0x706D3138, + bmdAudioMeterTypePPMMinus20db = /* 'pm20' */ 0x706D3230 +}; + +/* Enum BMDDeckLinkAttributeID - DeckLink Attribute ID */ + +typedef uint32_t BMDDeckLinkAttributeID; +enum _BMDDeckLinkAttributeID { + + /* Flags */ + + BMDDeckLinkSupportsInternalKeying = /* 'keyi' */ 0x6B657969, + BMDDeckLinkSupportsExternalKeying = /* 'keye' */ 0x6B657965, + BMDDeckLinkSupportsInputFormatDetection = /* 'infd' */ 0x696E6664, + BMDDeckLinkHasReferenceInput = /* 'hrin' */ 0x6872696E, + BMDDeckLinkHasSerialPort = /* 'hspt' */ 0x68737074, + BMDDeckLinkHasAnalogVideoOutputGain = /* 'avog' */ 0x61766F67, + BMDDeckLinkCanOnlyAdjustOverallVideoOutputGain = /* 'ovog' */ 0x6F766F67, + BMDDeckLinkHasVideoInputAntiAliasingFilter = /* 'aafl' */ 0x6161666C, + BMDDeckLinkHasBypass = /* 'byps' */ 0x62797073, + BMDDeckLinkSupportsClockTimingAdjustment = /* 'ctad' */ 0x63746164, + BMDDeckLinkSupportsFullFrameReferenceInputTimingOffset = /* 'frin' */ 0x6672696E, + BMDDeckLinkSupportsSMPTELevelAOutput = /* 'lvla' */ 0x6C766C61, + BMDDeckLinkSupportsAutoSwitchingPPsFOnInput = /* 'apsf' */ 0x61707366, + BMDDeckLinkSupportsDualLinkSDI = /* 'sdls' */ 0x73646C73, + BMDDeckLinkSupportsQuadLinkSDI = /* 'sqls' */ 0x73716C73, + BMDDeckLinkSupportsIdleOutput = /* 'idou' */ 0x69646F75, + BMDDeckLinkVANCRequires10BitYUVVideoFrames = /* 'vioY' */ 0x76696F59, // Legacy product requires v210 active picture for IDeckLinkVideoFrameAncillaryPackets or 10-bit VANC + BMDDeckLinkHasLTCTimecodeInput = /* 'hltc' */ 0x686C7463, + BMDDeckLinkSupportsHDRMetadata = /* 'hdrm' */ 0x6864726D, + BMDDeckLinkSupportsColorspaceMetadata = /* 'cmet' */ 0x636D6574, + BMDDeckLinkSupportsHDMITimecode = /* 'htim' */ 0x6874696D, + BMDDeckLinkSupportsHighFrameRateTimecode = /* 'HFRT' */ 0x48465254, + BMDDeckLinkSupportsSynchronizeToCaptureGroup = /* 'stcg' */ 0x73746367, + BMDDeckLinkSupportsSynchronizeToPlaybackGroup = /* 'stpg' */ 0x73747067, + BMDDeckLinkHasMonitorOut = /* 'fmoo' */ 0x666D6F6F, + BMDDeckLinkSupportsExtendedDesktop = /* 'dtop' */ 0x64746F70, + BMDDeckLinkHANCRequiresInputFilterConfiguration = /* 'hrif' */ 0x68726966, + BMDDeckLinkSupportsHANCOutput = /* 'dsho' */ 0x6473686F, + BMDDeckLinkSupportsHANCInput = /* 'dshi' */ 0x64736869, + + /* Integers */ + + BMDDeckLinkMaximumAudioChannels = /* 'mach' */ 0x6D616368, + BMDDeckLinkMaximumHDMIAudioChannels = /* 'mhch' */ 0x6D686368, + BMDDeckLinkMaximumAnalogAudioInputChannels = /* 'iach' */ 0x69616368, + BMDDeckLinkMaximumAnalogAudioOutputChannels = /* 'aach' */ 0x61616368, + BMDDeckLinkNumberOfSubDevices = /* 'nsbd' */ 0x6E736264, + BMDDeckLinkNumberOfEthernetConnectors = /* 'neth' */ 0x6E657468, + BMDDeckLinkSubDeviceIndex = /* 'subi' */ 0x73756269, + BMDDeckLinkPersistentID = /* 'peid' */ 0x70656964, + BMDDeckLinkDeviceGroupID = /* 'dgid' */ 0x64676964, + BMDDeckLinkTopologicalID = /* 'toid' */ 0x746F6964, + BMDDeckLinkVideoOutputConnections = /* 'vocn' */ 0x766F636E, // Returns a BMDVideoConnection bit field + BMDDeckLinkVideoInputConnections = /* 'vicn' */ 0x7669636E, // Returns a BMDVideoConnection bit field + BMDDeckLinkAudioOutputConnections = /* 'aocn' */ 0x616F636E, // Returns a BMDAudioConnection bit field + BMDDeckLinkAudioInputConnections = /* 'aicn' */ 0x6169636E, // Returns a BMDAudioConnection bit field + BMDDeckLinkVideoIOSupport = /* 'vios' */ 0x76696F73, // Returns a BMDVideoIOSupport bit field + BMDDeckLinkDeckControlConnections = /* 'dccn' */ 0x6463636E, // Returns a BMDDeckControlConnection bit field + BMDDeckLinkDeviceInterface = /* 'dbus' */ 0x64627573, // Returns a BMDDeviceInterface + BMDDeckLinkAudioInputRCAChannelCount = /* 'airc' */ 0x61697263, + BMDDeckLinkAudioInputXLRChannelCount = /* 'aixc' */ 0x61697863, + BMDDeckLinkAudioOutputRCAChannelCount = /* 'aorc' */ 0x616F7263, + BMDDeckLinkAudioOutputXLRChannelCount = /* 'aoxc' */ 0x616F7863, + BMDDeckLinkProfileID = /* 'prid' */ 0x70726964, // Returns a BMDProfileID + BMDDeckLinkDuplex = /* 'dupx' */ 0x64757078, + BMDDeckLinkMinimumPrerollFrames = /* 'mprf' */ 0x6D707266, + BMDDeckLinkSupportedDynamicRange = /* 'sudr' */ 0x73756472, + BMDDeckLinkMezzanineType = /* 'mezt' */ 0x6D657A74, + BMDDeckLinkXLRDelayMsMaximum = /* 'xdtx' */ 0x78647478, + BMDDeckLinkXLRDelayFramesMaximum = /* 'xdfx' */ 0x78646678, + BMDDeckLinkOutputHANCUserDataWordsLimit = /* 'mhow' */ 0x6D686F77, + BMDDeckLinkInputHANCUserDataWordsLimit = /* 'mhiw' */ 0x6D686977, + + /* Floats */ + + BMDDeckLinkVideoInputGainMinimum = /* 'vigm' */ 0x7669676D, + BMDDeckLinkVideoInputGainMaximum = /* 'vigx' */ 0x76696778, + BMDDeckLinkVideoOutputGainMinimum = /* 'vogm' */ 0x766F676D, + BMDDeckLinkVideoOutputGainMaximum = /* 'vogx' */ 0x766F6778, + BMDDeckLinkMicrophoneInputGainMinimum = /* 'migm' */ 0x6D69676D, + BMDDeckLinkMicrophoneInputGainMaximum = /* 'migx' */ 0x6D696778, + + /* Strings */ + + BMDDeckLinkSerialPortDeviceName = /* 'slpn' */ 0x736C706E, + BMDDeckLinkVendorName = /* 'vndr' */ 0x766E6472, + BMDDeckLinkDisplayName = /* 'dspn' */ 0x6473706E, + BMDDeckLinkModelName = /* 'mdln' */ 0x6D646C6E, + BMDDeckLinkDeviceHandle = /* 'devh' */ 0x64657668, + + /* Parameterized Strings */ + + BMDDeckLinkParamEthernetMACAddress = /* 'pMAC' */ 0x704D4143 +}; + +/* Enum BMDDeckLinkAPIInformationID - DeckLinkAPI information ID */ + +typedef uint32_t BMDDeckLinkAPIInformationID; +enum _BMDDeckLinkAPIInformationID { + + /* Integer or String */ + + BMDDeckLinkAPIVersion = /* 'vers' */ 0x76657273 +}; + +/* Enum BMDDeckLinkStatisticID - DeckLink Statistic ID */ + +typedef uint32_t BMDDeckLinkStatisticID; +enum _BMDDeckLinkStatisticID { + + /* Integers */ + + bmdDeckLinkStatisticPTPLossOfLock = /* 'nlol' */ 0x6E6C6F6C, + bmdDeckLinkStatisticPTPDPLLMarginOfError = /* 'ptpe' */ 0x70747065, + bmdDeckLinkStatisticDeviceTemperature = /* 'Stmp' */ 0x53746D70, + + /* Parameterized Integers */ + + bmdDeckLinkStatisticParamEthernetRxPackets = /* 'ntrx' */ 0x6E747278, + bmdDeckLinkStatisticParamEthernetRxDroppedPackets = /* 'ndrx' */ 0x6E647278, + + /* Parameterized Strings */ + + bmdDeckLinkStatisticParamEthernetSFPDynamicInfo = /* 'sfps' */ 0x73667073 +}; + +/* Enum BMDDeckLinkStatusID - DeckLink Status ID */ + +typedef uint32_t BMDDeckLinkStatusID; +enum _BMDDeckLinkStatusID { + + /* Integers / Interfaces */ + + bmdDeckLinkStatusDetectedVideoInputMode = /* 'dvim' */ 0x6476696D, + bmdDeckLinkStatusCurrentVideoInputMode = /* 'cvim' */ 0x6376696D, + bmdDeckLinkStatusCurrentVideoOutputMode = /* 'cvom' */ 0x63766F6D, + bmdDeckLinkStatusHDMIOutputActualMode = /* 'hiam' */ 0x6869616D, + + /* Integers */ + + bmdDeckLinkStatusDetectedVideoInputFormatFlags = /* 'dvff' */ 0x64766666, + bmdDeckLinkStatusDetectedVideoInputFieldDominance = /* 'dvfd' */ 0x64766664, + bmdDeckLinkStatusDetectedVideoInputColorspace = /* 'dscl' */ 0x6473636C, + bmdDeckLinkStatusDetectedVideoInputDynamicRange = /* 'dsdr' */ 0x64736472, + bmdDeckLinkStatusDetectedSDILinkConfiguration = /* 'dslc' */ 0x64736C63, + bmdDeckLinkStatusCurrentVideoInputPixelFormat = /* 'cvip' */ 0x63766970, + bmdDeckLinkStatusCurrentVideoInputFlags = /* 'cvif' */ 0x63766966, + bmdDeckLinkStatusCurrentVideoOutputFlags = /* 'cvof' */ 0x63766F66, + bmdDeckLinkStatusPCIExpressLinkWidth = /* 'pwid' */ 0x70776964, + bmdDeckLinkStatusPCIExpressLinkSpeed = /* 'plnk' */ 0x706C6E6B, + bmdDeckLinkStatusLastVideoOutputPixelFormat = /* 'opix' */ 0x6F706978, + bmdDeckLinkStatusReferenceSignalMode = /* 'refm' */ 0x7265666D, + bmdDeckLinkStatusReferenceSignalFlags = /* 'reff' */ 0x72656666, + bmdDeckLinkStatusBusy = /* 'busy' */ 0x62757379, + bmdDeckLinkStatusInterchangeablePanelType = /* 'icpt' */ 0x69637074, + bmdDeckLinkStatusHDMIOutputActualFormatFlags = /* 'hiaf' */ 0x68696166, + bmdDeckLinkStatusHDMIOutputFRLRate = /* 'hiof' */ 0x68696F66, + bmdDeckLinkStatusHDMIInputFRLRate = /* 'hiif' */ 0x68696966, + bmdDeckLinkStatusEthernetManualNMOSRegistry = /* 'nmme' */ 0x6E6D6D65, + bmdDeckLinkStatusHDMIOutputTMDSLineRate = /* 'hilr' */ 0x68696C72, + + /* Floats */ + + bmdDeckLinkStatusSinkSupportsDolbyVision = /* 'dvvr' */ 0x64767672, + + /* Flags */ + + bmdDeckLinkStatusVideoInputSignalLocked = /* 'visl' */ 0x7669736C, + bmdDeckLinkStatusAncillaryInputSignalLocked = /* 'aisl' */ 0x6169736C, + bmdDeckLinkStatusReferenceSignalLocked = /* 'refl' */ 0x7265666C, + + /* Strings */ + + bmdDeckLinkStatusEthernetPTPGrandmasterIdentity = /* 'spid' */ 0x73706964, + bmdDeckLinkStatusEthernetAudioInputChannelOrder = /* 'saco' */ 0x7361636F, + bmdDeckLinkStatusEthernetCurrentNMOSRegistry = /* 'nmre' */ 0x6E6D7265, + + /* Bytes */ + + bmdDeckLinkStatusReceivedEDID = /* 'edid' */ 0x65646964, + + /* Parameterized Integers */ + + bmdDeckLinkStatusParamEthernetLink = /* 'sels' */ 0x73656C73, + bmdDeckLinkStatusParamEthernetLinkMbps = /* 'sesp' */ 0x73657370, + + /* Parameterized Strings */ + + bmdDeckLinkStatusParamEthernetLocalIPAddress = /* 'seip' */ 0x73656970, + bmdDeckLinkStatusParamEthernetSubnetMask = /* 'sesm' */ 0x7365736D, + bmdDeckLinkStatusParamEthernetGatewayIPAddress = /* 'segw' */ 0x73656777, + bmdDeckLinkStatusParamEthernetPrimaryDNS = /* 'sepd' */ 0x73657064, + bmdDeckLinkStatusParamEthernetSecondaryDNS = /* 'sesd' */ 0x73657364, + bmdDeckLinkStatusParamEthernetSFPStaticInfo = /* 'sfpi' */ 0x73667069, + bmdDeckLinkStatusParamEthernetVideoOutputAddress = /* 'soav' */ 0x736F6176, + bmdDeckLinkStatusParamEthernetAudioOutputAddress = /* 'soaa' */ 0x736F6161, + bmdDeckLinkStatusParamEthernetAncillaryOutputAddress = /* 'soaA' */ 0x736F6141 +}; + +/* Enum BMDDeckLinkVideoStatusFlags - */ + +typedef uint32_t BMDDeckLinkVideoStatusFlags; +enum _BMDDeckLinkVideoStatusFlags { + bmdDeckLinkVideoStatusPsF = 1 << 0, + bmdDeckLinkVideoStatusDualStream3D = 1 << 1 +}; + +/* Enum BMDDuplexMode - Duplex of the device */ + +typedef uint32_t BMDDuplexMode; +enum _BMDDuplexMode { + bmdDuplexFull = /* 'dxfu' */ 0x64786675, + bmdDuplexHalf = /* 'dxha' */ 0x64786861, + bmdDuplexSimplex = /* 'dxsp' */ 0x64787370, + bmdDuplexInactive = /* 'dxin' */ 0x6478696E +}; + +/* Enum BMDPanelType - The type of interchangeable panel */ + +typedef uint32_t BMDPanelType; +enum _BMDPanelType { + bmdPanelNotDetected = /* 'npnl' */ 0x6E706E6C, + bmdPanelTeranexMiniSmartPanel = /* 'tmsm' */ 0x746D736D +}; + +/* Enum BMDFormatFlags - Flags to describe the video signal */ + +typedef uint32_t BMDFormatFlags; +enum _BMDFormatFlags { + bmdFormatRGB444 = 1 << 0, + bmdFormatYUV444 = 1 << 1, + bmdFormatYUV422 = 1 << 2, + bmdFormatYUV420 = 1 << 3, + bmdFormat8BitDepth = 1 << 4, + bmdFormat10BitDepth = 1 << 5, + bmdFormat12BitDepth = 1 << 6 +}; + +/* Enum BMDDeviceBusyState - Current device busy state */ + +typedef uint32_t BMDDeviceBusyState; +enum _BMDDeviceBusyState { + bmdDeviceCaptureBusy = 1 << 0, + bmdDevicePlaybackBusy = 1 << 1, + bmdDeviceSerialPortBusy = 1 << 2 +}; + +/* Enum BMDVideoIOSupport - Device video input/output support */ + +typedef uint32_t BMDVideoIOSupport; +enum _BMDVideoIOSupport { + bmdDeviceSupportsCapture = 1 << 0, + bmdDeviceSupportsPlayback = 1 << 1 +}; + +/* Enum BMD3DPreviewFormat - Linked Frame preview format */ + +typedef uint32_t BMD3DPreviewFormat; +enum _BMD3DPreviewFormat { + bmd3DPreviewFormatDefault = /* 'defa' */ 0x64656661, + bmd3DPreviewFormatLeftOnly = /* 'left' */ 0x6C656674, + bmd3DPreviewFormatRightOnly = /* 'righ' */ 0x72696768, + bmd3DPreviewFormatSideBySide = /* 'side' */ 0x73696465, + bmd3DPreviewFormatTopBottom = /* 'topb' */ 0x746F7062 +}; + +/* Enum BMDAncillaryDataSpace - BMDAncillaryDataSpace enumerates the location of an ancillary packet. */ + +typedef uint32_t BMDAncillaryDataSpace; +enum _BMDAncillaryDataSpace { + bmdAncillaryDataSpaceVANC = 0, + bmdAncillaryDataSpaceHANC = 1 +}; + +/* Enum BMDIPFlowDirection - BMDIPFlowDirection enumerates the direction of the IP flow. */ + +enum BMDIPFlowDirection { + bmdDeckLinkIPFlowDirectionOutput = 0, + bmdDeckLinkIPFlowDirectionInput = 1 +}; + +/* Enum BMDIPFlowType - BMDIPFlowType enumerates the IP flow type. */ + +enum BMDIPFlowType { + bmdDeckLinkIPFlowTypeVideo = 0, + bmdDeckLinkIPFlowTypeAudio = 1, + bmdDeckLinkIPFlowTypeAncillary = 2 +}; + +/* Enum BMDDeckLinkIPFlowAttributeID - DeckLink IP Flow Attribute ID */ + +enum BMDDeckLinkIPFlowAttributeID { + + /* DeckLink IP Flow Attribute Integers */ + + bmdDeckLinkIPFlowID = /* '2fai' */ 0x32666169, + bmdDeckLinkIPFlowDirection = /* '2fad' */ 0x32666164, + bmdDeckLinkIPFlowType = /* '2fat' */ 0x32666174 +}; + +/* Enum BMDDeckLinkIPFlowStatusID - DeckLink IP Flow Attribute ID */ + +enum BMDDeckLinkIPFlowStatusID { + + /* DeckLink IP Flow Status Strings */ + + bmdDeckLinkIPFlowSDP = /* '2fas' */ 0x32666173 +}; + +/* Enum BMDDeckLinkIPFlowSettingID - DeckLink IP Flow Setting ID */ + +enum BMDDeckLinkIPFlowSettingID { + + /* DeckLink IP Flow Setting Strings */ + + bmdDeckLinkIPFlowPeerSDP = /* '2fps' */ 0x32667073 // The peer's SDP. Must not be over 1000 bytes large. +}; + +/* Enum BMDNotifications - Events that can be subscribed through IDeckLinkNotification */ + +typedef uint32_t BMDNotifications; +enum _BMDNotifications { + bmdPreferencesChanged = /* 'pref' */ 0x70726566, + bmdStatusChanged = /* 'stat' */ 0x73746174, + bmdIPFlowStatusChanged = /* 'bfsc' */ 0x62667363, + bmdIPFlowSettingChanged = /* 'bfcc' */ 0x62666363 +}; + +#if defined(__cplusplus) + +// Forward Declarations + +class IDeckLinkVideoOutputCallback; +class IDeckLinkInputCallback; +class IDeckLinkEncoderInputCallback; +class IDeckLinkVideoBufferAllocator; +class IDeckLinkVideoBufferAllocatorProvider; +class IDeckLinkAudioOutputCallback; +class IDeckLinkIterator; +class IDeckLinkAPIInformation; +class IDeckLinkIPFlowAttributes; +class IDeckLinkIPFlowStatus; +class IDeckLinkIPFlowSetting; +class IDeckLinkIPFlow; +class IDeckLinkIPFlowIterator; +class IDeckLinkOutput; +class IDeckLinkInput; +class IDeckLinkIPExtensions; +class IDeckLinkHDMIInputEDID; +class IDeckLinkEncoderInput; +class IDeckLinkVideoBuffer; +class IDeckLinkVideoFrame; +class IDeckLinkMutableVideoFrame; +class IDeckLinkVideoFrame3DExtensions; +class IDeckLinkVideoFrameMetadataExtensions; +class IDeckLinkVideoFrameMutableMetadataExtensions; +class IDeckLinkVideoInputFrame; +class IDeckLinkAncillaryPacket; +class IDeckLinkAncillaryPacketIterator; +class IDeckLinkVideoFrameAncillaryPackets; +class IDeckLinkVideoFrameAncillary; +class IDeckLinkEncoderPacket; +class IDeckLinkEncoderVideoPacket; +class IDeckLinkEncoderAudioPacket; +class IDeckLinkH265NALPacket; +class IDeckLinkAudioInputPacket; +class IDeckLinkScreenPreviewCallback; +class IDeckLinkGLScreenPreviewHelper; +class IDeckLinkNotificationCallback; +class IDeckLinkNotification; +class IDeckLinkProfileAttributes; +class IDeckLinkProfileIterator; +class IDeckLinkProfile; +class IDeckLinkProfileCallback; +class IDeckLinkProfileManager; +class IDeckLinkStatistics; +class IDeckLinkStatus; +class IDeckLinkKeyer; +class IDeckLinkVideoConversion; +class IDeckLinkDeviceNotificationCallback; +class IDeckLinkDiscovery; + +/* Interface IDeckLinkVideoOutputCallback - Frame completion callback. */ + +class BMD_PUBLIC IDeckLinkVideoOutputCallback : public IUnknown +{ +public: + virtual HRESULT ScheduledFrameCompleted (/* in */ IDeckLinkVideoFrame* completedFrame, /* in */ BMDOutputFrameCompletionResult result) = 0; + virtual HRESULT ScheduledPlaybackHasStopped (void) = 0; + +protected: + virtual ~IDeckLinkVideoOutputCallback () {} // call Release method to drop reference count +}; + +/* Interface IDeckLinkInputCallback - Frame arrival callback. */ + +class BMD_PUBLIC IDeckLinkInputCallback : public IUnknown +{ +public: + virtual HRESULT VideoInputFormatChanged (/* in */ BMDVideoInputFormatChangedEvents notificationEvents, /* in */ IDeckLinkDisplayMode* newDisplayMode, /* in */ BMDDetectedVideoInputFormatFlags detectedSignalFlags) = 0; + virtual HRESULT VideoInputFrameArrived (/* in */ IDeckLinkVideoInputFrame* videoFrame, /* in */ IDeckLinkAudioInputPacket* audioPacket) = 0; + +protected: + virtual ~IDeckLinkInputCallback () {} // call Release method to drop reference count +}; + +/* Interface IDeckLinkEncoderInputCallback - Frame arrival callback. */ + +class BMD_PUBLIC IDeckLinkEncoderInputCallback : public IUnknown +{ +public: + virtual HRESULT VideoInputSignalChanged (/* in */ BMDVideoInputFormatChangedEvents notificationEvents, /* in */ IDeckLinkDisplayMode* newDisplayMode, /* in */ BMDDetectedVideoInputFormatFlags detectedSignalFlags) = 0; + virtual HRESULT VideoPacketArrived (/* in */ IDeckLinkEncoderVideoPacket* videoPacket) = 0; + virtual HRESULT AudioPacketArrived (/* in */ IDeckLinkEncoderAudioPacket* audioPacket) = 0; + +protected: + virtual ~IDeckLinkEncoderInputCallback () {} // call Release method to drop reference count +}; + +/* Interface IDeckLinkVideoBufferAllocator - Buffer allocator for video. */ + +class BMD_PUBLIC IDeckLinkVideoBufferAllocator : public IUnknown +{ +public: + virtual HRESULT AllocateVideoBuffer (/* out */ IDeckLinkVideoBuffer** allocatedBuffer) = 0; + +protected: + virtual ~IDeckLinkVideoBufferAllocator () {} // call Release method to drop reference count +}; + +/* Interface IDeckLinkVideoBufferAllocatorProvider - Allows EnableVideoInputWithAllocatorProvider to obtain allocators */ + +class BMD_PUBLIC IDeckLinkVideoBufferAllocatorProvider : 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** allocator) = 0; + +protected: + virtual ~IDeckLinkVideoBufferAllocatorProvider () {} // call Release method to drop reference count +}; + +/* Interface IDeckLinkAudioOutputCallback - Optional callback to allow audio samples to be pulled as required. */ + +class BMD_PUBLIC IDeckLinkAudioOutputCallback : public IUnknown +{ +public: + virtual HRESULT RenderAudioSamples (/* in */ bool preroll) = 0; +}; + +/* Interface IDeckLinkIterator - Enumerates installed DeckLink hardware */ + +class BMD_PUBLIC IDeckLinkIterator : public IUnknown +{ +public: + virtual HRESULT Next (/* out */ IDeckLink** deckLinkInstance) = 0; +}; + +/* Interface IDeckLinkAPIInformation - DeckLinkAPI attribute interface */ + +class BMD_PUBLIC IDeckLinkAPIInformation : public IUnknown +{ +public: + virtual HRESULT GetFlag (/* in */ BMDDeckLinkAPIInformationID cfgID, /* out */ bool* value) = 0; + virtual HRESULT GetInt (/* in */ BMDDeckLinkAPIInformationID cfgID, /* out */ int64_t* value) = 0; + virtual HRESULT GetFloat (/* in */ BMDDeckLinkAPIInformationID cfgID, /* out */ double* value) = 0; + virtual HRESULT GetString (/* in */ BMDDeckLinkAPIInformationID cfgID, /* out */ const char** value) = 0; + +protected: + virtual ~IDeckLinkAPIInformation () {} // call Release method to drop reference count +}; + +/* Interface IDeckLinkIPFlowAttributes - */ + +class BMD_PUBLIC IDeckLinkIPFlowAttributes : public IUnknown +{ +public: + virtual HRESULT GetInt (/* in */ BMDDeckLinkIPFlowAttributeID attrID, /* out */ int64_t* value) = 0; + virtual HRESULT GetFlag (/* in */ BMDDeckLinkIPFlowAttributeID attrID, /* out */ bool* value) = 0; + virtual HRESULT GetFloat (/* in */ BMDDeckLinkIPFlowAttributeID attrID, /* out */ double* value) = 0; + virtual HRESULT GetString (/* in */ BMDDeckLinkIPFlowAttributeID attrID, /* out */ const char** value) = 0; + +protected: + virtual ~IDeckLinkIPFlowAttributes () {} // call Release method to drop reference count +}; + +/* Interface IDeckLinkIPFlowStatus - */ + +class BMD_PUBLIC IDeckLinkIPFlowStatus : public IUnknown +{ +public: + virtual HRESULT GetInt (/* in */ BMDDeckLinkIPFlowStatusID statusID, /* out */ int64_t* value) = 0; + virtual HRESULT GetFlag (/* in */ BMDDeckLinkIPFlowStatusID statusID, /* out */ bool* value) = 0; + virtual HRESULT GetFloat (/* in */ BMDDeckLinkIPFlowStatusID statusID, /* out */ double* value) = 0; + virtual HRESULT GetString (/* in */ BMDDeckLinkIPFlowStatusID statusID, /* out */ const char** value) = 0; + +protected: + virtual ~IDeckLinkIPFlowStatus () {} // call Release method to drop reference count +}; + +/* Interface IDeckLinkIPFlowSetting - */ + +class BMD_PUBLIC IDeckLinkIPFlowSetting : public IUnknown +{ +public: + virtual HRESULT GetInt (/* in */ BMDDeckLinkIPFlowSettingID settingID, /* out */ int64_t* value) = 0; + virtual HRESULT GetFlag (/* in */ BMDDeckLinkIPFlowSettingID settingID, /* out */ bool* value) = 0; + virtual HRESULT GetFloat (/* in */ BMDDeckLinkIPFlowSettingID settingID, /* out */ double* value) = 0; + virtual HRESULT GetString (/* in */ BMDDeckLinkIPFlowSettingID settingID, /* out */ const char** value) = 0; + virtual HRESULT SetInt (/* in */ BMDDeckLinkIPFlowSettingID settingID, /* in */ int64_t value) = 0; + virtual HRESULT SetFlag (/* in */ BMDDeckLinkIPFlowSettingID settingID, /* in */ bool value) = 0; + virtual HRESULT SetFloat (/* in */ BMDDeckLinkIPFlowSettingID settingID, /* in */ double value) = 0; + virtual HRESULT SetString (/* in */ BMDDeckLinkIPFlowSettingID settingID, /* in */ const char* value) = 0; + +protected: + virtual ~IDeckLinkIPFlowSetting () {} // call Release method to drop reference count +}; + +/* Interface IDeckLinkIPFlow - */ + +class BMD_PUBLIC IDeckLinkIPFlow : public IUnknown +{ +public: + virtual HRESULT Enable (void) = 0; // Enables an IP flow to start sending or receiving. + virtual HRESULT Disable (void) = 0; // Disables an IP flow to stop sending or receiving. + +protected: + virtual ~IDeckLinkIPFlow () {} // call Release method to drop reference count +}; + +/* Interface IDeckLinkIPFlowIterator - Enumerates DeckLink IP Flows */ + +class BMD_PUBLIC IDeckLinkIPFlowIterator : public IUnknown +{ +public: + virtual HRESULT Next (/* out */ IDeckLinkIPFlow** deckLinkIPFlowInstance) = 0; + +protected: + virtual ~IDeckLinkIPFlowIterator () {} // call Release method to drop reference count +}; + +/* Interface IDeckLinkOutput - Created by QueryInterface from IDeckLink. */ + +class BMD_PUBLIC IDeckLinkOutput : 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* 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; // Deprecated. 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 () {} // call Release method to drop reference count +}; + +/* Interface IDeckLinkInput - Created by QueryInterface from IDeckLink. */ + +class BMD_PUBLIC IDeckLinkInput : 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* 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 () {} // call Release method to drop reference count +}; + +/* Interface IDeckLinkIPExtensions - */ + +class BMD_PUBLIC IDeckLinkIPExtensions : public IUnknown +{ +public: + virtual HRESULT GetDeckLinkIPFlowIterator (/* out */ IDeckLinkIPFlowIterator** iterator) = 0; + virtual HRESULT GetIPFlowByID (/* in */ BMDIPFlowID id, /* out */ IDeckLinkIPFlow** flow) = 0; + +protected: + virtual ~IDeckLinkIPExtensions () {} // call Release method to drop reference count +}; + +/* Interface IDeckLinkHDMIInputEDID - Created by QueryInterface from IDeckLink. Releasing all references will restore EDID to default */ + +class BMD_PUBLIC IDeckLinkHDMIInputEDID : public IUnknown +{ +public: + virtual HRESULT SetInt (/* in */ BMDDeckLinkHDMIInputEDIDID cfgID, /* in */ int64_t value) = 0; + virtual HRESULT GetInt (/* in */ BMDDeckLinkHDMIInputEDIDID cfgID, /* out */ int64_t* value) = 0; + virtual HRESULT WriteToEDID (void) = 0; + +protected: + virtual ~IDeckLinkHDMIInputEDID () {} // call Release method to drop reference count +}; + +/* Interface IDeckLinkEncoderInput - Created by QueryInterface from IDeckLink. */ + +class BMD_PUBLIC IDeckLinkEncoderInput : 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; + + /* 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 () {} // call Release method to drop reference count +}; + +/* Interface IDeckLinkVideoBuffer - Interface to encapsulate a video frame buffer; can be caller-implemented. */ + +class BMD_PUBLIC IDeckLinkVideoBuffer : public IUnknown +{ +public: + virtual HRESULT GetBytes (/* out */ void** buffer) = 0; + virtual HRESULT GetSize (/* out */ uint64_t* size) = 0; + virtual HRESULT StartAccess (/* in */ BMDBufferAccessFlags flags) = 0; + virtual HRESULT EndAccess (/* in */ BMDBufferAccessFlags flags) = 0; + +protected: + virtual ~IDeckLinkVideoBuffer () {} // call Release method to drop reference count +}; + +/* Interface IDeckLinkVideoFrame - Interface to encapsulate a video frame; can be caller-implemented. */ + +class BMD_PUBLIC IDeckLinkVideoFrame : 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 GetTimecode (/* in */ BMDTimecodeFormat format, /* out */ IDeckLinkTimecode** timecode) = 0; + virtual HRESULT GetAncillaryData (/* out */ IDeckLinkVideoFrameAncillary** ancillary) = 0; // Deprecated. Use of IDeckLinkVideoFrameAncillaryPackets is preferred + +protected: + virtual ~IDeckLinkVideoFrame () {} // call Release method to drop reference count +}; + +/* Interface IDeckLinkMutableVideoFrame - Created by IDeckLinkOutput::CreateVideoFrame. */ + +class BMD_PUBLIC IDeckLinkMutableVideoFrame : public IDeckLinkVideoFrame +{ +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; // Deprecated. Use of IDeckLinkVideoFrameAncillaryPackets is preferred + virtual HRESULT SetTimecodeUserBits (/* in */ BMDTimecodeFormat format, /* in */ BMDTimecodeUserBits userBits) = 0; + virtual HRESULT SetInterfaceProvider (/* in */ REFIID iid, /* in */ IUnknown* iface) = 0; + +protected: + virtual ~IDeckLinkMutableVideoFrame () {} // call Release method to drop reference count +}; + +/* Interface IDeckLinkVideoFrame3DExtensions - Optional interface to support 3D frames. */ + +class BMD_PUBLIC IDeckLinkVideoFrame3DExtensions : public IUnknown +{ +public: + virtual BMDVideo3DPackingFormat Get3DPackingFormat (void) = 0; + virtual HRESULT GetFrameForRightEye (/* out */ IDeckLinkVideoFrame** rightEyeFrame) = 0; + +protected: + virtual ~IDeckLinkVideoFrame3DExtensions () {} // call Release method to drop reference count +}; + +/* Interface IDeckLinkVideoFrameMetadataExtensions - Get frame metadata */ + +class BMD_PUBLIC IDeckLinkVideoFrameMetadataExtensions : public IUnknown +{ +public: + virtual HRESULT GetInt (/* in */ BMDDeckLinkFrameMetadataID metadataID, /* out */ int64_t* value) = 0; + virtual HRESULT GetFloat (/* in */ BMDDeckLinkFrameMetadataID metadataID, /* out */ double* value) = 0; + virtual HRESULT GetFlag (/* in */ BMDDeckLinkFrameMetadataID metadataID, /* out */ bool* value) = 0; + virtual HRESULT GetString (/* in */ BMDDeckLinkFrameMetadataID metadataID, /* out */ const char** value) = 0; + virtual HRESULT GetBytes (/* in */ BMDDeckLinkFrameMetadataID metadataID, /* out */ void* buffer /* optional */, /* in, out */ uint32_t* bufferSize) = 0; + +protected: + virtual ~IDeckLinkVideoFrameMetadataExtensions () {} // call Release method to drop reference count +}; + +/* Interface IDeckLinkVideoFrameMutableMetadataExtensions - Set frame metadata */ + +class BMD_PUBLIC IDeckLinkVideoFrameMutableMetadataExtensions : public IDeckLinkVideoFrameMetadataExtensions +{ +public: + virtual HRESULT SetInt (/* in */ BMDDeckLinkFrameMetadataID metadataID, /* in */ int64_t value) = 0; + virtual HRESULT SetFloat (/* in */ BMDDeckLinkFrameMetadataID metadataID, /* in */ double value) = 0; + virtual HRESULT SetFlag (/* in */ BMDDeckLinkFrameMetadataID metadataID, /* in */ bool value) = 0; + virtual HRESULT SetString (/* in */ BMDDeckLinkFrameMetadataID metadataID, /* in */ const char* value) = 0; + virtual HRESULT SetBytes (/* in */ BMDDeckLinkFrameMetadataID metadataID, /* in */ void* buffer, /* in */ uint32_t bufferSize) = 0; + +protected: + virtual ~IDeckLinkVideoFrameMutableMetadataExtensions () {} // call Release method to drop reference count +}; + +/* Interface IDeckLinkVideoInputFrame - Provided by the IDeckLinkVideoInput frame arrival callback. */ + +class BMD_PUBLIC IDeckLinkVideoInputFrame : public IDeckLinkVideoFrame +{ +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 () {} // call Release method to drop reference count +}; + +/* Interface IDeckLinkAncillaryPacket - On output, user needs to implement this interface */ + +class BMD_PUBLIC IDeckLinkAncillaryPacket : 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 + virtual BMDAncillaryDataSpace GetDataSpace (void) = 0; + +protected: + virtual ~IDeckLinkAncillaryPacket () {} // call Release method to drop reference count +}; + +/* Interface IDeckLinkAncillaryPacketIterator - Enumerates ancillary packets */ + +class BMD_PUBLIC IDeckLinkAncillaryPacketIterator : public IUnknown +{ +public: + virtual HRESULT Next (/* out */ IDeckLinkAncillaryPacket** packet) = 0; + +protected: + virtual ~IDeckLinkAncillaryPacketIterator () {} // call Release method to drop reference count +}; + +/* Interface IDeckLinkVideoFrameAncillaryPackets - Obtained through QueryInterface on an IDeckLinkVideoFrame object. */ + +class BMD_PUBLIC IDeckLinkVideoFrameAncillaryPackets : public IUnknown +{ +public: + virtual HRESULT GetPacketIterator (/* out */ IDeckLinkAncillaryPacketIterator** iterator) = 0; + virtual HRESULT GetFirstPacketByID (/* in */ uint8_t DID, /* in */ uint8_t SDID, /* out */ IDeckLinkAncillaryPacket** packet) = 0; + virtual HRESULT AttachPacket (/* in */ IDeckLinkAncillaryPacket* packet) = 0; // Implement IDeckLinkAncillaryPacket to output your own + virtual HRESULT DetachPacket (/* in */ IDeckLinkAncillaryPacket* packet) = 0; + virtual HRESULT DetachAllPackets (void) = 0; + +protected: + virtual ~IDeckLinkVideoFrameAncillaryPackets () {} // call Release method to drop reference count +}; + +/* Interface IDeckLinkVideoFrameAncillary - Use of IDeckLinkVideoFrameAncillaryPackets is preferred. Obtained through QueryInterface on an IDeckLinkVideoFrame object. */ + +class BMD_PUBLIC IDeckLinkVideoFrameAncillary : public IUnknown +{ +public: + virtual HRESULT GetBufferForVerticalBlankingLine (/* in */ uint32_t lineNumber, /* out */ void** buffer) = 0; // Pixels/rowbytes is same as display mode, except for above HD where it's 1920 pixels for UHD modes and 2048 pixels for DCI modes + virtual BMDPixelFormat GetPixelFormat (void) = 0; + virtual BMDDisplayMode GetDisplayMode (void) = 0; + +protected: + virtual ~IDeckLinkVideoFrameAncillary () {} // call Release method to drop reference count +}; + +/* Interface IDeckLinkEncoderPacket - Interface to encapsulate an encoded packet. */ + +class BMD_PUBLIC IDeckLinkEncoderPacket : public IUnknown +{ +public: + virtual HRESULT GetBytes (/* out */ void** buffer) = 0; + virtual long GetSize (void) = 0; + virtual HRESULT GetStreamTime (/* out */ BMDTimeValue* frameTime, /* in */ BMDTimeScale timeScale) = 0; + virtual BMDPacketType GetPacketType (void) = 0; + +protected: + virtual ~IDeckLinkEncoderPacket () {} // call Release method to drop reference count +}; + +/* Interface IDeckLinkEncoderVideoPacket - Provided by the IDeckLinkEncoderInput video packet arrival callback. */ + +class BMD_PUBLIC IDeckLinkEncoderVideoPacket : public IDeckLinkEncoderPacket +{ +public: + virtual BMDPixelFormat GetPixelFormat (void) = 0; + virtual HRESULT GetHardwareReferenceTimestamp (/* in */ BMDTimeScale timeScale, /* out */ BMDTimeValue* frameTime, /* out */ BMDTimeValue* frameDuration) = 0; + virtual HRESULT GetTimecode (/* in */ BMDTimecodeFormat format, /* out */ IDeckLinkTimecode** timecode) = 0; + +protected: + virtual ~IDeckLinkEncoderVideoPacket () {} // call Release method to drop reference count +}; + +/* Interface IDeckLinkEncoderAudioPacket - Provided by the IDeckLinkEncoderInput audio packet arrival callback. */ + +class BMD_PUBLIC IDeckLinkEncoderAudioPacket : public IDeckLinkEncoderPacket +{ +public: + virtual BMDAudioFormat GetAudioFormat (void) = 0; + +protected: + virtual ~IDeckLinkEncoderAudioPacket () {} // call Release method to drop reference count +}; + +/* Interface IDeckLinkH265NALPacket - Obtained through QueryInterface on an IDeckLinkEncoderVideoPacket object */ + +class BMD_PUBLIC IDeckLinkH265NALPacket : public IDeckLinkEncoderVideoPacket +{ +public: + virtual HRESULT GetUnitType (/* out */ uint8_t* unitType) = 0; + virtual HRESULT GetBytesNoPrefix (/* out */ void** buffer) = 0; + virtual long GetSizeNoPrefix (void) = 0; + +protected: + virtual ~IDeckLinkH265NALPacket () {} // call Release method to drop reference count +}; + +/* Interface IDeckLinkAudioInputPacket - Provided by the IDeckLinkInput callback. */ + +class BMD_PUBLIC IDeckLinkAudioInputPacket : public IUnknown +{ +public: + virtual long GetSampleFrameCount (void) = 0; + virtual HRESULT GetBytes (/* out */ void** buffer) = 0; + virtual HRESULT GetPacketTime (/* out */ BMDTimeValue* packetTime, /* in */ BMDTimeScale timeScale) = 0; + +protected: + virtual ~IDeckLinkAudioInputPacket () {} // call Release method to drop reference count +}; + +/* Interface IDeckLinkScreenPreviewCallback - Screen preview callback */ + +class BMD_PUBLIC IDeckLinkScreenPreviewCallback : public IUnknown +{ +public: + virtual HRESULT DrawFrame (/* in */ IDeckLinkVideoFrame* theFrame) = 0; + +protected: + virtual ~IDeckLinkScreenPreviewCallback () {} // call Release method to drop reference count +}; + +/* Interface IDeckLinkGLScreenPreviewHelper - Created with CoCreateInstance on platforms with native COM support or from CreateOpenGLScreenPreviewHelper/CreateOpenGL3ScreenPreviewHelper on other platforms. */ + +class BMD_PUBLIC IDeckLinkGLScreenPreviewHelper : 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* theFrame) = 0; + virtual HRESULT Set3DPreviewFormat (/* in */ BMD3DPreviewFormat previewFormat) = 0; + +protected: + virtual ~IDeckLinkGLScreenPreviewHelper () {} // call Release method to drop reference count +}; + +/* Interface IDeckLinkNotificationCallback - DeckLink Notification Callback Interface */ + +class BMD_PUBLIC IDeckLinkNotificationCallback : public IUnknown +{ +public: + virtual HRESULT Notify (/* in */ BMDNotifications topic, /* in */ uint64_t param1, /* in */ uint64_t param2) = 0; +}; + +/* Interface IDeckLinkNotification - DeckLink Notification interface */ + +class BMD_PUBLIC IDeckLinkNotification : 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 () {} // call Release method to drop reference count +}; + +/* Interface IDeckLinkProfileAttributes - Created by QueryInterface from an IDeckLinkProfile, or from IDeckLink. When queried from IDeckLink, interrogates the active profile */ + +class BMD_PUBLIC IDeckLinkProfileAttributes : 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; + virtual HRESULT GetStringWithParam (/* in */ BMDDeckLinkAttributeID cfgID, /* in */ uint64_t param, /* out */ const char** value) = 0; + +protected: + virtual ~IDeckLinkProfileAttributes () {} // call Release method to drop reference count +}; + +/* Interface IDeckLinkProfileIterator - Enumerates IDeckLinkProfile interfaces */ + +class BMD_PUBLIC IDeckLinkProfileIterator : public IUnknown +{ +public: + virtual HRESULT Next (/* out */ IDeckLinkProfile** profile) = 0; + +protected: + virtual ~IDeckLinkProfileIterator () {} // call Release method to drop reference count +}; + +/* Interface IDeckLinkProfile - Represents the active profile when queried from IDeckLink */ + +class BMD_PUBLIC IDeckLinkProfile : public IUnknown +{ +public: + virtual HRESULT GetDevice (/* out */ IDeckLink** device) = 0; // Device affected when this profile becomes active + virtual HRESULT IsActive (/* out */ bool* isActive) = 0; + virtual HRESULT SetActive (void) = 0; // Activating a profile will also change the profile on all devices enumerated by GetPeers. Activation is not complete until IDeckLinkProfileCallback::ProfileActivated is called + virtual HRESULT GetPeers (/* out */ IDeckLinkProfileIterator** profileIterator) = 0; // Profiles of other devices activated with this profile + +protected: + virtual ~IDeckLinkProfile () {} // call Release method to drop reference count +}; + +/* Interface IDeckLinkProfileCallback - Receive notifications about profiles related to this device */ + +class BMD_PUBLIC IDeckLinkProfileCallback : public IUnknown +{ +public: + virtual HRESULT ProfileChanging (/* in */ IDeckLinkProfile* profileToBeActivated, /* in */ bool streamsWillBeForcedToStop) = 0; // Called before this device changes profile. User has an opportunity for teardown if streamsWillBeForcedToStop + virtual HRESULT ProfileActivated (/* in */ IDeckLinkProfile* activatedProfile) = 0; // Called after this device has been activated with a new profile + +protected: + virtual ~IDeckLinkProfileCallback () {} // call Release method to drop reference count +}; + +/* Interface IDeckLinkProfileManager - Created by QueryInterface from IDeckLink when a device has multiple optional profiles */ + +class BMD_PUBLIC IDeckLinkProfileManager : public IUnknown +{ +public: + virtual HRESULT GetProfiles (/* out */ IDeckLinkProfileIterator** profileIterator) = 0; // All available profiles for this device + virtual HRESULT GetProfile (/* in */ BMDProfileID profileID, /* out */ IDeckLinkProfile** profile) = 0; + virtual HRESULT SetCallback (/* in */ IDeckLinkProfileCallback* callback) = 0; + +protected: + virtual ~IDeckLinkProfileManager () {} // call Release method to drop reference count +}; + +/* Interface IDeckLinkStatistics - DeckLink Statistics interface */ + +class BMD_PUBLIC IDeckLinkStatistics : public IUnknown +{ +public: + virtual HRESULT GetInt (/* in */ BMDDeckLinkStatisticID statID, /* out */ int64_t* value) = 0; + virtual HRESULT GetIntWithParam (/* in */ BMDDeckLinkStatisticID statID, /* in */ uint64_t param, /* out */ int64_t* value) = 0; + virtual HRESULT GetStringWithParam (/* in */ BMDDeckLinkStatisticID statID, /* in */ uint64_t param, /* out */ const char** value) = 0; + +protected: + virtual ~IDeckLinkStatistics () {} // call Release method to drop reference count +}; + +/* Interface IDeckLinkStatus - DeckLink Status interface */ + +class BMD_PUBLIC IDeckLinkStatus : 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; + virtual HRESULT GetInterface (/* in */ BMDDeckLinkStatusID statusID, /* out */ void** iface) = 0; + virtual HRESULT GetFlagWithParam (/* in */ BMDDeckLinkStatusID statusID, /* in */ uint64_t param, /* out */ bool* value) = 0; + virtual HRESULT GetIntWithParam (/* in */ BMDDeckLinkStatusID statusID, /* in */ uint64_t param, /* out */ int64_t* value) = 0; + virtual HRESULT GetFloatWithParam (/* in */ BMDDeckLinkStatusID statusID, /* in */ uint64_t param, /* out */ double* value) = 0; + virtual HRESULT GetStringWithParam (/* in */ BMDDeckLinkStatusID statusID, /* in */ uint64_t param, /* out */ const char** value) = 0; + virtual HRESULT GetBytesWithParam (/* in */ BMDDeckLinkStatusID statusID, /* in */ uint64_t param, /* out */ void* buffer, /* in, out */ uint32_t* bufferSize) = 0; + +protected: + virtual ~IDeckLinkStatus () {} // call Release method to drop reference count +}; + +/* Interface IDeckLinkKeyer - DeckLink Keyer interface */ + +class BMD_PUBLIC IDeckLinkKeyer : public IUnknown +{ +public: + virtual HRESULT Enable (/* in */ bool isExternal) = 0; + virtual HRESULT SetLevel (/* in */ uint8_t level) = 0; + virtual HRESULT RampUp (/* in */ uint32_t numberOfFrames) = 0; + virtual HRESULT RampDown (/* in */ uint32_t numberOfFrames) = 0; + virtual HRESULT Disable (void) = 0; + +protected: + virtual ~IDeckLinkKeyer () {} // call Release method to drop reference count +}; + +/* Interface IDeckLinkVideoConversion - Created with CoCreateInstance. */ + +class BMD_PUBLIC IDeckLinkVideoConversion : 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* dstBuffer, /* out */ IDeckLinkVideoFrame** dstFrame) = 0; + +protected: + virtual ~IDeckLinkVideoConversion () {} // call Release method to drop reference count +}; + +/* Interface IDeckLinkDeviceNotificationCallback - DeckLink device arrival/removal notification callbacks */ + +class BMD_PUBLIC IDeckLinkDeviceNotificationCallback : public IUnknown +{ +public: + virtual HRESULT DeckLinkDeviceArrived (/* in */ IDeckLink* deckLinkDevice) = 0; + virtual HRESULT DeckLinkDeviceRemoved (/* in */ IDeckLink* deckLinkDevice) = 0; + +protected: + virtual ~IDeckLinkDeviceNotificationCallback () {} // call Release method to drop reference count +}; + +/* Interface IDeckLinkDiscovery - DeckLink device discovery */ + +class BMD_PUBLIC IDeckLinkDiscovery : public IUnknown +{ +public: + virtual HRESULT InstallDeviceNotifications (/* in */ IDeckLinkDeviceNotificationCallback* deviceNotificationCallback) = 0; + virtual HRESULT UninstallDeviceNotifications (void) = 0; + +protected: + virtual ~IDeckLinkDiscovery () {} // call Release method to drop reference count +}; + +/* Functions */ + +extern "C" { + + BMD_PUBLIC IDeckLinkIterator* CreateDeckLinkIteratorInstance(void); + BMD_PUBLIC IDeckLinkDiscovery* CreateDeckLinkDiscoveryInstance(void); + BMD_PUBLIC IDeckLinkAPIInformation* CreateDeckLinkAPIInformationInstance(void); + BMD_PUBLIC IDeckLinkGLScreenPreviewHelper* CreateOpenGLScreenPreviewHelper(void); + BMD_PUBLIC IDeckLinkGLScreenPreviewHelper* CreateOpenGL3ScreenPreviewHelper(void); // Requires OpenGL 3.2 support and provides improved performance and color handling + BMD_PUBLIC IDeckLinkVideoConversion* CreateVideoConversionInstance(void); + BMD_PUBLIC IDeckLinkVideoFrameAncillaryPackets* CreateVideoFrameAncillaryPacketsInstance(void); // For use when creating a custom IDeckLinkVideoFrame without wrapping IDeckLinkOutput::CreateVideoFrame + +} + + + +#endif /* defined(__cplusplus) */ +#endif /* defined(BMD_DECKLINKAPI_H) */ diff --git a/services/capture/sdk/DeckLinkAPIConfiguration.h b/services/capture/sdk/DeckLinkAPIConfiguration.h new file mode 100644 index 0000000..955b4a8 --- /dev/null +++ b/services/capture/sdk/DeckLinkAPIConfiguration.h @@ -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) */ diff --git a/services/capture/sdk/DeckLinkAPIConfiguration_v10_11.h b/services/capture/sdk/DeckLinkAPIConfiguration_v10_11.h new file mode 100644 index 0000000..3c6c59b --- /dev/null +++ b/services/capture/sdk/DeckLinkAPIConfiguration_v10_11.h @@ -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) */ diff --git a/services/capture/sdk/DeckLinkAPIConfiguration_v10_2.h b/services/capture/sdk/DeckLinkAPIConfiguration_v10_2.h new file mode 100644 index 0000000..326ba63 --- /dev/null +++ b/services/capture/sdk/DeckLinkAPIConfiguration_v10_2.h @@ -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) */ diff --git a/services/capture/sdk/DeckLinkAPIConfiguration_v10_4.h b/services/capture/sdk/DeckLinkAPIConfiguration_v10_4.h new file mode 100644 index 0000000..9ce1adc --- /dev/null +++ b/services/capture/sdk/DeckLinkAPIConfiguration_v10_4.h @@ -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) */ diff --git a/services/capture/sdk/DeckLinkAPIConfiguration_v10_5.h b/services/capture/sdk/DeckLinkAPIConfiguration_v10_5.h new file mode 100644 index 0000000..71feda5 --- /dev/null +++ b/services/capture/sdk/DeckLinkAPIConfiguration_v10_5.h @@ -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) */ diff --git a/services/capture/sdk/DeckLinkAPIConfiguration_v10_9.h b/services/capture/sdk/DeckLinkAPIConfiguration_v10_9.h new file mode 100644 index 0000000..79ccfe8 --- /dev/null +++ b/services/capture/sdk/DeckLinkAPIConfiguration_v10_9.h @@ -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) */ diff --git a/services/capture/sdk/DeckLinkAPIConfiguration_v15_3_1.h b/services/capture/sdk/DeckLinkAPIConfiguration_v15_3_1.h new file mode 100644 index 0000000..f1546da --- /dev/null +++ b/services/capture/sdk/DeckLinkAPIConfiguration_v15_3_1.h @@ -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 +}; diff --git a/services/capture/sdk/DeckLinkAPIDeckControl.h b/services/capture/sdk/DeckLinkAPIDeckControl.h new file mode 100644 index 0000000..1a3f413 --- /dev/null +++ b/services/capture/sdk/DeckLinkAPIDeckControl.h @@ -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) */ diff --git a/services/capture/sdk/DeckLinkAPIDiscovery.h b/services/capture/sdk/DeckLinkAPIDiscovery.h new file mode 100644 index 0000000..b519db8 --- /dev/null +++ b/services/capture/sdk/DeckLinkAPIDiscovery.h @@ -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) */ diff --git a/services/capture/sdk/DeckLinkAPIDispatch.cpp b/services/capture/sdk/DeckLinkAPIDispatch.cpp new file mode 100644 index 0000000..ed54632 --- /dev/null +++ b/services/capture/sdk/DeckLinkAPIDispatch.cpp @@ -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 +#include +#include + +#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(); +} diff --git a/services/capture/sdk/DeckLinkAPIDispatch_v10_11.cpp b/services/capture/sdk/DeckLinkAPIDispatch_v10_11.cpp new file mode 100644 index 0000000..7ef0f7e --- /dev/null +++ b/services/capture/sdk/DeckLinkAPIDispatch_v10_11.cpp @@ -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 +#include +#include + +#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(); +} diff --git a/services/capture/sdk/DeckLinkAPIDispatch_v10_8.cpp b/services/capture/sdk/DeckLinkAPIDispatch_v10_8.cpp new file mode 100644 index 0000000..08286ad --- /dev/null +++ b/services/capture/sdk/DeckLinkAPIDispatch_v10_8.cpp @@ -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 +#include +#include + +#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(); +} diff --git a/services/capture/sdk/DeckLinkAPIDispatch_v14_2_1.cpp b/services/capture/sdk/DeckLinkAPIDispatch_v14_2_1.cpp new file mode 100644 index 0000000..7a65443 --- /dev/null +++ b/services/capture/sdk/DeckLinkAPIDispatch_v14_2_1.cpp @@ -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 +#include +#include + +#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(); +} diff --git a/services/capture/sdk/DeckLinkAPIDispatch_v15_2.cpp b/services/capture/sdk/DeckLinkAPIDispatch_v15_2.cpp new file mode 100644 index 0000000..a87a150 --- /dev/null +++ b/services/capture/sdk/DeckLinkAPIDispatch_v15_2.cpp @@ -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 +#include +#include + +#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(); +} diff --git a/services/capture/sdk/DeckLinkAPIDispatch_v15_3_1.cpp b/services/capture/sdk/DeckLinkAPIDispatch_v15_3_1.cpp new file mode 100644 index 0000000..f780f5b --- /dev/null +++ b/services/capture/sdk/DeckLinkAPIDispatch_v15_3_1.cpp @@ -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 +#include +#include + +#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(); +} diff --git a/services/capture/sdk/DeckLinkAPIGLScreenPreview_v14_2_1.h b/services/capture/sdk/DeckLinkAPIGLScreenPreview_v14_2_1.h new file mode 100644 index 0000000..98f9e23 --- /dev/null +++ b/services/capture/sdk/DeckLinkAPIGLScreenPreview_v14_2_1.h @@ -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 diff --git a/services/capture/sdk/DeckLinkAPIMemoryAllocator_v14_2_1.h b/services/capture/sdk/DeckLinkAPIMemoryAllocator_v14_2_1.h new file mode 100644 index 0000000..dc6fded --- /dev/null +++ b/services/capture/sdk/DeckLinkAPIMemoryAllocator_v14_2_1.h @@ -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 diff --git a/services/capture/sdk/DeckLinkAPIMetalScreenPreview_v14_2_1.h b/services/capture/sdk/DeckLinkAPIMetalScreenPreview_v14_2_1.h new file mode 100644 index 0000000..928564d --- /dev/null +++ b/services/capture/sdk/DeckLinkAPIMetalScreenPreview_v14_2_1.h @@ -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 diff --git a/services/capture/sdk/DeckLinkAPIModes.h b/services/capture/sdk/DeckLinkAPIModes.h new file mode 100644 index 0000000..2dec531 --- /dev/null +++ b/services/capture/sdk/DeckLinkAPIModes.h @@ -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) */ diff --git a/services/capture/sdk/DeckLinkAPIScreenPreviewCallback_v14_2_1.h b/services/capture/sdk/DeckLinkAPIScreenPreviewCallback_v14_2_1.h new file mode 100644 index 0000000..9ab8f7d --- /dev/null +++ b/services/capture/sdk/DeckLinkAPIScreenPreviewCallback_v14_2_1.h @@ -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) */ diff --git a/services/capture/sdk/DeckLinkAPITypes.h b/services/capture/sdk/DeckLinkAPITypes.h new file mode 100644 index 0000000..57a8d91 --- /dev/null +++ b/services/capture/sdk/DeckLinkAPITypes.h @@ -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) */ diff --git a/services/capture/sdk/DeckLinkAPIVersion.h b/services/capture/sdk/DeckLinkAPIVersion.h new file mode 100644 index 0000000..49e2ff1 --- /dev/null +++ b/services/capture/sdk/DeckLinkAPIVersion.h @@ -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__ + diff --git a/services/capture/sdk/DeckLinkAPIVideoConversion_v14_2_1.h b/services/capture/sdk/DeckLinkAPIVideoConversion_v14_2_1.h new file mode 100644 index 0000000..7c1f057 --- /dev/null +++ b/services/capture/sdk/DeckLinkAPIVideoConversion_v14_2_1.h @@ -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 diff --git a/services/capture/sdk/DeckLinkAPIVideoEncoderInput_v10_11.h b/services/capture/sdk/DeckLinkAPIVideoEncoderInput_v10_11.h new file mode 100644 index 0000000..64312d0 --- /dev/null +++ b/services/capture/sdk/DeckLinkAPIVideoEncoderInput_v10_11.h @@ -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) */ diff --git a/services/capture/sdk/DeckLinkAPIVideoFrame3DExtensions_v14_2_1.h b/services/capture/sdk/DeckLinkAPIVideoFrame3DExtensions_v14_2_1.h new file mode 100644 index 0000000..7407a6b --- /dev/null +++ b/services/capture/sdk/DeckLinkAPIVideoFrame3DExtensions_v14_2_1.h @@ -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) */ diff --git a/services/capture/sdk/DeckLinkAPIVideoFrame_v14_2_1.h b/services/capture/sdk/DeckLinkAPIVideoFrame_v14_2_1.h new file mode 100644 index 0000000..902be76 --- /dev/null +++ b/services/capture/sdk/DeckLinkAPIVideoFrame_v14_2_1.h @@ -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) */ diff --git a/services/capture/sdk/DeckLinkAPIVideoInput_v10_11.h b/services/capture/sdk/DeckLinkAPIVideoInput_v10_11.h new file mode 100644 index 0000000..91c0aea --- /dev/null +++ b/services/capture/sdk/DeckLinkAPIVideoInput_v10_11.h @@ -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) */ diff --git a/services/capture/sdk/DeckLinkAPIVideoInput_v11_4.h b/services/capture/sdk/DeckLinkAPIVideoInput_v11_4.h new file mode 100644 index 0000000..2fc8191 --- /dev/null +++ b/services/capture/sdk/DeckLinkAPIVideoInput_v11_4.h @@ -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) */ diff --git a/services/capture/sdk/DeckLinkAPIVideoInput_v11_5_1.h b/services/capture/sdk/DeckLinkAPIVideoInput_v11_5_1.h new file mode 100644 index 0000000..d0f8944 --- /dev/null +++ b/services/capture/sdk/DeckLinkAPIVideoInput_v11_5_1.h @@ -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) */ diff --git a/services/capture/sdk/DeckLinkAPIVideoInput_v14_2_1.h b/services/capture/sdk/DeckLinkAPIVideoInput_v14_2_1.h new file mode 100644 index 0000000..0653759 --- /dev/null +++ b/services/capture/sdk/DeckLinkAPIVideoInput_v14_2_1.h @@ -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) */ diff --git a/services/capture/sdk/DeckLinkAPIVideoInput_v15_3_1.h b/services/capture/sdk/DeckLinkAPIVideoInput_v15_3_1.h new file mode 100644 index 0000000..6c570d9 --- /dev/null +++ b/services/capture/sdk/DeckLinkAPIVideoInput_v15_3_1.h @@ -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 +}; diff --git a/services/capture/sdk/DeckLinkAPIVideoOutput_v10_11.h b/services/capture/sdk/DeckLinkAPIVideoOutput_v10_11.h new file mode 100644 index 0000000..8a4d1ec --- /dev/null +++ b/services/capture/sdk/DeckLinkAPIVideoOutput_v10_11.h @@ -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) */ diff --git a/services/capture/sdk/DeckLinkAPIVideoOutput_v11_4.h b/services/capture/sdk/DeckLinkAPIVideoOutput_v11_4.h new file mode 100644 index 0000000..be3230d --- /dev/null +++ b/services/capture/sdk/DeckLinkAPIVideoOutput_v11_4.h @@ -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) */ diff --git a/services/capture/sdk/DeckLinkAPIVideoOutput_v14_2_1.h b/services/capture/sdk/DeckLinkAPIVideoOutput_v14_2_1.h new file mode 100644 index 0000000..981a057 --- /dev/null +++ b/services/capture/sdk/DeckLinkAPIVideoOutput_v14_2_1.h @@ -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 diff --git a/services/capture/sdk/DeckLinkAPIVideoOutput_v15_3_1.h b/services/capture/sdk/DeckLinkAPIVideoOutput_v15_3_1.h new file mode 100644 index 0000000..cd9bff6 --- /dev/null +++ b/services/capture/sdk/DeckLinkAPIVideoOutput_v15_3_1.h @@ -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) diff --git a/services/capture/sdk/DeckLinkAPI_v10_11.h b/services/capture/sdk/DeckLinkAPI_v10_11.h new file mode 100644 index 0000000..cf7ce8f --- /dev/null +++ b/services/capture/sdk/DeckLinkAPI_v10_11.h @@ -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) */ diff --git a/services/capture/sdk/DeckLinkAPI_v10_2.h b/services/capture/sdk/DeckLinkAPI_v10_2.h new file mode 100644 index 0000000..126e461 --- /dev/null +++ b/services/capture/sdk/DeckLinkAPI_v10_2.h @@ -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) */ diff --git a/services/capture/sdk/DeckLinkAPI_v10_4.h b/services/capture/sdk/DeckLinkAPI_v10_4.h new file mode 100644 index 0000000..59be138 --- /dev/null +++ b/services/capture/sdk/DeckLinkAPI_v10_4.h @@ -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) */ diff --git a/services/capture/sdk/DeckLinkAPI_v10_5.h b/services/capture/sdk/DeckLinkAPI_v10_5.h new file mode 100644 index 0000000..35dee6a --- /dev/null +++ b/services/capture/sdk/DeckLinkAPI_v10_5.h @@ -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) */ + diff --git a/services/capture/sdk/DeckLinkAPI_v10_6.h b/services/capture/sdk/DeckLinkAPI_v10_6.h new file mode 100644 index 0000000..58aae5d --- /dev/null +++ b/services/capture/sdk/DeckLinkAPI_v10_6.h @@ -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) */ diff --git a/services/capture/sdk/DeckLinkAPI_v10_9.h b/services/capture/sdk/DeckLinkAPI_v10_9.h new file mode 100644 index 0000000..347b908 --- /dev/null +++ b/services/capture/sdk/DeckLinkAPI_v10_9.h @@ -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) */ diff --git a/services/capture/sdk/DeckLinkAPI_v11_5.h b/services/capture/sdk/DeckLinkAPI_v11_5.h new file mode 100644 index 0000000..521cf65 --- /dev/null +++ b/services/capture/sdk/DeckLinkAPI_v11_5.h @@ -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) */ diff --git a/services/capture/sdk/DeckLinkAPI_v11_5_1.h b/services/capture/sdk/DeckLinkAPI_v11_5_1.h new file mode 100644 index 0000000..2e91033 --- /dev/null +++ b/services/capture/sdk/DeckLinkAPI_v11_5_1.h @@ -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) */ diff --git a/services/capture/sdk/DeckLinkAPI_v14_2_1.h b/services/capture/sdk/DeckLinkAPI_v14_2_1.h new file mode 100644 index 0000000..ec2ceb9 --- /dev/null +++ b/services/capture/sdk/DeckLinkAPI_v14_2_1.h @@ -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) */ diff --git a/services/capture/sdk/DeckLinkAPI_v15_2.h b/services/capture/sdk/DeckLinkAPI_v15_2.h new file mode 100644 index 0000000..c7bfdd4 --- /dev/null +++ b/services/capture/sdk/DeckLinkAPI_v15_2.h @@ -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) */ diff --git a/services/capture/sdk/DeckLinkAPI_v15_3_1.h b/services/capture/sdk/DeckLinkAPI_v15_3_1.h new file mode 100644 index 0000000..b91dead --- /dev/null +++ b/services/capture/sdk/DeckLinkAPI_v15_3_1.h @@ -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) diff --git a/services/capture/sdk/LinuxCOM.h b/services/capture/sdk/LinuxCOM.h new file mode 100644 index 0000000..1bdaf7f --- /dev/null +++ b/services/capture/sdk/LinuxCOM.h @@ -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 + diff --git a/services/capture/src/capture-manager.js b/services/capture/src/capture-manager.js index 50108d8..ae76051 100644 --- a/services/capture/src/capture-manager.js +++ b/services/capture/src/capture-manager.js @@ -1,9 +1,11 @@ import { spawn, execFileSync } from 'child_process'; -import { mkdirSync, writeFileSync } from 'node:fs'; +import { mkdirSync, writeFileSync, createReadStream, statSync, unlinkSync } from 'node:fs'; +import fs from 'node:fs'; import { dirname } from 'node:path'; import { v4 as uuidv4 } from 'uuid'; import { createUploadStream } from './s3/client.js'; + const S3_BUCKET = process.env.S3_BUCKET || 'wild-dragon'; // Growing-files mode: writes the master to a local SMB-backed share that the @@ -19,7 +21,19 @@ const GROWING_PATH = process.env.GROWING_PATH || '/growing'; // by the API from global Settings. Empty GROWING_SMB_MOUNT → no system mount // (the host-bound /growing volume is used instead, or S3 streaming if growing // is off). -const GROWING_SMB_MOUNT = process.env.GROWING_SMB_MOUNT || ''; +// mount.cifs needs a UNC source (//host/share). Operators (and Settings) often +// store the share as an `smb://host/share` URL or a Windows `\\host\share` +// path; the kernel rejects those outright ("Mounting cifs URL not implemented +// yet"), which silently drops us back to S3. Normalize any of these forms to +// the `//host/share` UNC the mount helper accepts. +function toUncShare(raw) { + if (!raw) return ''; + let s = String(raw).trim().replace(/\\/g, '/'); // \\host\share -> //host/share + s = s.replace(/^smb:\/\//i, '//'); // smb://host/share -> //host/share + if (!s.startsWith('//')) s = '//' + s.replace(/^\/+/, ''); // host/share -> //host/share + return s; +} +const GROWING_SMB_MOUNT = toUncShare(process.env.GROWING_SMB_MOUNT || ''); const GROWING_SMB_USERNAME = process.env.GROWING_SMB_USERNAME || ''; const GROWING_SMB_PASSWORD = process.env.GROWING_SMB_PASSWORD || ''; const GROWING_SMB_VERS = process.env.GROWING_SMB_VERS || '3.0'; @@ -121,6 +135,59 @@ const VIDEO_CODECS = { }, }; +// nvenc codecs available in the capture image. Used both to validate the master +// codec and (issue #164) as the GPU-availability signal for the HLS preview. +const NVENC_CODECS = new Set(['h264_nvenc', 'hevc_nvenc']); + +// ── GPU availability for this sidecar (issue #164) ─────────────────────── +// The HLS monitor preview should be GPU-encoded (h264_nvenc) when — and only +// when — the GPU is actually attached to this capture container. A non-GPU +// recorder must keep using libx264, otherwise ffmpeg would fail to open the +// nvenc encoder and break the preview. +// +// Two signals, OR'd for robustness: +// 1) The master video codec is an nvenc codec. recorders.js derives `useGpu` +// from exactly this (GPU_CODECS = [hevc_nvenc, h264_nvenc]) and node-agent +// only attaches the NVIDIA runtime when useGpu is set — so an nvenc master +// codec is a reliable proxy for "this sidecar has the GPU". +// 2) node-agent injects NVIDIA_VISIBLE_DEVICES into the sidecar env whenever +// useGpu is set. This is the most direct in-process evidence the runtime +// attached a GPU, and covers the (currently unused) case where the GPU is +// present but the master codec is a CPU codec. +function gpuAvailableForPreview(masterCodec) { + if (NVENC_CODECS.has(masterCodec)) return true; + const vis = process.env.NVIDIA_VISIBLE_DEVICES; + if (vis && vis !== 'void' && vis !== 'none') return true; + return false; +} + +// Build the HLS preview video-encode args. `segTime` is the HLS segment length +// (seconds); we pin the GOP/keyframe interval to one IDR per segment so every +// segment starts on a keyframe (misaligned keyframes were the root cause of the +// playout preview black/flashing bug — keep the preview robust). +function buildHlsVideoArgs(masterCodec, framerate) { + // Frames-per-segment for keyframe alignment. The SDI preview runs at the + // capture framerate; default to 30 (matches the test-card rate) when unknown. + const fps = Number.parseFloat(framerate) || 30; + const segTime = 2; // matches -hls_time below + const gop = Math.max(1, Math.round(fps * segTime)); + if (gpuAvailableForPreview(masterCodec)) { + // Low-latency NVENC preset (p1 + ll tune). forced-idr + a keyframe every GOP + // frames keeps segment boundaries on IDR frames so hls.js can sync cleanly. + return [ + '-c:v', 'h264_nvenc', '-preset', 'p1', '-tune', 'll', + '-pix_fmt', 'yuv420p', '-b:v', '2M', + '-g', String(gop), '-forced-idr', '1', '-sc_threshold', '0', + ]; + } + // No GPU → keep the original CPU encode (must not break a non-GPU recorder). + return [ + '-c:v', 'libx264', '-preset', 'veryfast', '-tune', 'zerolatency', + '-pix_fmt', 'yuv420p', '-b:v', '2M', + '-g', String(gop), '-sc_threshold', '0', + ]; +} + const AUDIO_CODECS = { pcm_s16le: { args: ['-c:a', 'pcm_s16le'], bitrateControl: false }, pcm_s24le: { args: ['-c:a', 'pcm_s24le'], bitrateControl: false }, @@ -143,11 +210,262 @@ const CONTAINER_EXT = { mov: 'mov', mp4: 'mp4', mkv: 'mkv', mxf: 'mxf', ts: 'ts', }; +// Growing-file (edit-while-record) master format — MXF OP1a / XDCAM HD422, +// written by bmx (raw2bmx), NOT by ffmpeg's MXF muxer. +// +// This is the SIXTH iteration. The five prior attempts and WHY they failed +// (root-caused with authoritative sources + live structural analysis on the +// zampp2 capture image): +// +// 1) Fragmented MP4/MOV (+frag_keyframe+empty_moov): Premiere's QuickTime +// importer needs the classic stco/stsz/stts sample tables in one top-level +// moov; a fragmented MOV never has them while growing → "unable to open". +// +// 2) MXF OP1a / DNxHR HQ via ffmpeg: a DNxHR MXF SIGKILLed mid-write has ZERO +// body partitions and probes duration=N/A — DNxHR's large VBR frames don't +// trigger ffmpeg's per-partition flush, so only the header is on disk. +// +// 3) MPEG-TS H.264 High 4:2:2: Premiere's H.264 importer only accepts 8-bit +// 4:2:0. +// +// 4) MPEG-TS H.264 High 4:2:0 all-intra + AAC: STILL "unsupported compression +// type" — Premiere does not treat a raw .ts elementary stream as a clean +// importable growing clip. +// +// 5) MXF OP1a / XDCAM HD422 (MPEG-2 422) via ffmpeg's `-f mxf` muxer: this was +// believed to flush incremental body partitions, but PROVEN unable to +// produce a TRUE growing file — ffmpeg's MXF muxer writes the real +// duration/index only in the FOOTER at av_write_trailer (close). A +// metadata-only probe of the mid-write file reports duration=N/A right up +// until the writer exits, so Premiere's growing-file refresh never sees the +// file extend. (Same muxer that defers the index to EOF.) +// +// FIX — MXF OP1a carrying XDCAM HD422 (MPEG-2 422 @ 50 Mbps-class) + PCM, muxed +// by bmx/raw2bmx (the reference growing-OP1a writer, used by BBC/broadcast): +// +// WHY raw2bmx (the key discovery, PROVEN live on zampp2): +// * raw2bmx with `-t op1a --part ` writes a NEW body partition PLUS +// a NEW IndexTableSegment (carrying an updated IndexDuration) at the +// interval. The recorded duration is therefore readable — and INCREASES — +// from the header+index ALONE while the file is still being written, no +// footer needed. Verified by snapshotting the growing file mid-write and +// parsing the IndexTableSegment IndexDuration (local tag 0x3F0C): +// T= 3s: 7 partitions, max IndexDuration = 43 frames +// T= 8s: 17 partitions, max IndexDuration = 193 frames +// T=15s: 31 partitions, max IndexDuration = 403 frames +// The recorded frame count grows monotonically, lagging the record head by +// ~one partition interval — exactly the editable-head behaviour Premiere's +// growing-MXF reader consumes. A mid-write snapshot also decodes cleanly +// (mpeg2video 1920x1080 + 2×PCM, ffmpeg decode exit 0). Contrast with the +// ffmpeg `-f mxf` path (attempt #5): duration=N/A until close. +// * Adobe OFFICIALLY recommends MXF for growing-file workflows; XDCAM HD422 +// (MPEG-2 422 in MXF OP1a) + PCM is read by Premiere's built-in MXF reader +// with no plugin and is the broadcast-standard growing acquisition format. +// +// Pipeline (single SDI read — DeckLink cannot be opened twice): +// ffmpeg decklink → yadif → split → +// (a) MPEG-2 422 elementary VIDEO → named FIFO ┐ +// (b) PCM s16le AUDIO → named FIFO ├→ raw2bmx -t op1a +// (c) H.264 HLS preview (unchanged, keeps monitor live) +// raw2bmx reads the two essence FIFOs and writes the growing OP1a MXF to the +// CIFS share. On stop, ffmpeg is stopped cleanly so raw2bmx gets EOF and +// finalizes the footer; we await raw2bmx exit before reporting complete. +// +// Audio: PCM s16le — the native, broadcast-standard MXF audio mapping +// Premiere's MXF reader expects (NOT AAC). +// +// HONEST CAVEAT (cannot be verified without real Premiere on the workstation): +// the growing IndexDuration / body-partition structure is PROVEN above and +// matches Adobe's documented growing-MXF requirement — but only the user +// opening the growing .mxf in actual Premiere Pro (with "Automatically refresh +// growing files" enabled in Preferences > Media) can confirm the end-to-end +// edit-while-record. +// +// ── ffmpeg elementary-essence args (input to the FIFOs) ─────────────────── +// (a) MPEG-2 422, 8-bit 4:2:2 (Premiere-native XDCAM HD422). `-dc 10` + the CBR +// bitrate (operator target, default 50 Mbps) match XDCAM HD422 essence. `-g 15` +// keeps a short GOP. Muxed to a raw `mpeg2video` elementary stream (no +// container) so raw2bmx ingests it via --mpeg2lg_*. +const GROWING_VIDEO_ELEMENTARY_ARGS = [ + '-c:v', 'mpeg2video', '-pix_fmt', 'yuv422p', + '-dc', '10', '-g', '15', '-bf', '2', +]; +const GROWING_DEFAULT_BITRATE = '25M'; +const GROWING_EXT = 'mxf'; +// Video essence partition interval (frames). raw2bmx starts a new body partition +// + IndexTableSegment every PART_INTERVAL frames; this is the granularity at +// which the growing file's recorded duration advances. ~1s at 25/29.97 fps. +const GROWING_PART_INTERVAL_FRAMES = 30; + +// Map the recorder's resolution/fps to (1) the raw2bmx MPEG-2 Long GOP essence +// input flag and (2) the ffmpeg edit-rate (`-r`). raw2bmx needs the correct +// raster flag so the essence is wrapped as the right XDCAM HD422 variant; an +// 1080i59.94 default is used when the recorder fields are absent (the most +// common SDI broadcast raster). Returns: +// { rawFlag, frameRate, ffRate } +// where rawFlag is e.g. '--mpeg2lg_422p_hl_1080i', frameRate is the raw2bmx +// `-f` value (e.g. '30000/1001'), and ffRate is the ffmpeg `-r` value. +// +// NOTE: the exact interlaced-vs-progressive raster and the fps for a real +// DeckLink SDI feed can only be confirmed against the live signal. This derives +// a sensible value from the recorder's configured resolution/framerate; if those +// are absent or ambiguous it defaults to 1080i59.94. A live DeckLink confirm of +// the actual SDI raster/fps is advised before production use (see report). +function deriveGrowingRaster(resolution, framerate, scanHint = null) { + // Normalise fps. Accept '59.94', '60000/1001', '25', '50', '30', '29.97'… + let fpsNum = null; + const fr = (framerate == null) ? '' : String(framerate).trim(); + if (/^\d+\/\d+$/.test(fr)) { + const [n, d] = fr.split('/').map(Number); + if (d) fpsNum = n / d; + } else if (fr && fr !== 'native') { + const f = Number.parseFloat(fr); + if (Number.isFinite(f)) fpsNum = f; + } + + // Resolution → height + scan. Accept '1920x1080', '1080i', '1080p', '720p', + // '720', '576i', etc. + const res = (resolution == null) ? '' : String(resolution).trim().toLowerCase(); + let height = null; + let scan = null; // 'i' | 'p' | null + const mDim = res.match(/(\d{3,4})x(\d{3,4})/); + if (mDim) height = parseInt(mDim[2], 10); + const mH = res.match(/(\d{3,4})\s*([ip])/); + if (mH) { height = parseInt(mH[1], 10); scan = mH[2]; } + if (height == null) { + const only = res.match(/\b(2160|1080|720|576|480)\b/); + if (only) height = parseInt(only[1], 10); + } + if (height == null) height = 1080; // default raster + + // ffmpeg rate + raw2bmx rate strings for the common broadcast rates. + function rates(fps) { + if (fps == null) return { ff: '30000/1001', raw: '30000/1001' }; // 1080i59.94 default + if (Math.abs(fps - 59.94) < 0.2 || Math.abs(fps - 29.97) < 0.05) + return { ff: '30000/1001', raw: '30000/1001' }; + if (Math.abs(fps - 60) < 0.05) return { ff: '60', raw: '60' }; + if (Math.abs(fps - 50) < 0.05) return { ff: '25', raw: '25' }; // 1080i50 → 25 fps frames + if (Math.abs(fps - 25) < 0.05) return { ff: '25', raw: '25' }; + if (Math.abs(fps - 24) < 0.2) return { ff: '24000/1001', raw: '24000/1001' }; + if (Math.abs(fps - 30) < 0.05) return { ff: '30', raw: '30' }; + return { ff: String(fps), raw: String(fps) }; + } + + // Default scan: 1080 → interlaced (broadcast SDI default), 720/below → p. + // scanHint ('p'/'i') overrides this default so progressive Deltacast captures + // are wrapped as progressive MXFs. + if (scan == null) scan = scanHint || ((height >= 1080) ? 'i' : 'p'); + const r = rates(fpsNum); + + let rawFlag; + if (height >= 1080) { + rawFlag = (scan === 'p') ? '--mpeg2lg_422p_hl_1080p' : '--mpeg2lg_422p_hl_1080i'; + } else if (height >= 720) { + rawFlag = '--mpeg2lg_422p_hl_720p'; // 720 is always progressive + if (fpsNum == null) { r.ff = '60000/1001'; r.raw = '60000/1001'; } + } else { + rawFlag = '--mpeg2lg_422p_ml_576i'; // SD 576i (PAL); 25 fps + r.ff = '25'; r.raw = '25'; + } + + return { rawFlag, frameRate: r.raw, ffRate: r.ff }; +} + +// ── Source-backend abstraction (issue #168) ────────────────────────────── +// The capture input was historically hard-wired to a single `-f decklink -i …` +// construction. To allow other SDI capture cards (Deltacast, AJA) to be added +// later without touching the encode/output/HLS pipeline, the per-backend FFmpeg +// INPUT-arg construction now lives behind this map. Each backend exposes: +// +// buildInput(ctx) -> { inputArgs, isNetwork } (may be async) +// +// where `ctx` carries the resolved recorder fields the backend needs (device). +// The rest of capture-manager consumes the returned `inputArgs` unchanged, so +// adding a backend is purely additive. +// +// IMPORTANT: `blackmagic` is a behaviour-preserving extraction of the previous +// default DeckLink path — for an existing DeckLink recorder the produced ffmpeg +// input args are byte-for-byte identical to the pre-refactor code. The +// `deltacast`/`aja` entries are stubs that throw until the hardware/SDK plumbing +// lands. +const sourceBackends = { + // BlackMagic DeckLink over SDI (the only backend implemented today). + // device may be an integer index (0-based) or a full device name string. + // FFmpeg 7.x DeckLink requires the full name (e.g. 'DeckLink Duo 2 (2)'). + // Map integer index -> name using ffmpeg -sources decklink at runtime. + // + // ffmpeg -sources decklink output format: + // Auto-detected sources for decklink: + // DeckLink Duo 2 + // DeckLink Duo 2 (2) + // Lines containing device names start with whitespace; the header line + // starts with a non-space character. Previous code used a v4l2-style + // hex-address regex that never matched DeckLink output → index 1+ always + // fell through to a wrong fallback, producing black output from port 2+. + blackmagic: { + async buildInput({ device }) { + let deckLinkName = String(device); + if (typeof device === 'number' || /^\d+$/.test(String(device))) { + const idx = parseInt(device, 10); + try { + const { execSync } = await import('child_process'); + const out = execSync('ffmpeg -hide_banner -sources decklink 2>&1', { encoding: 'utf-8', timeout: 5000 }); + const names = []; + for (const line of out.split('\n')) { + // DeckLink source lines: " 81:76669a80:00000000 [DeckLink Duo (1)] (none)" + const m = line.match(/^\s+[0-9a-f:]+\s+\[([^\]]+)\]/); + if (m) names.push(m[1]); + } + if (names[idx]) { + deckLinkName = names[idx]; + console.log(`[capture] DeckLink index ${idx} → "${deckLinkName}" (from ${names.length} detected: ${names.join(', ')})`); + } else { + // Fallback: cannot determine model name without enumeration. + // Log a warning — operator should check the detected device list. + console.warn(`[capture] DeckLink index ${idx} out of range (detected ${names.length} devices: ${names.join(', ')}). Falling back to index-only input — capture may fail.`); + deckLinkName = `DeckLink (${idx})`; + } + } catch (err) { + console.warn(`[capture] ffmpeg -sources decklink failed: ${err.message}. Using index ${device} directly.`); + // Pass the numeric index directly; some ffmpeg builds accept it. + deckLinkName = String(device); + } + } + return { + inputArgs: ['-f', 'decklink', '-i', deckLinkName], + isNetwork: false, + }; + }, + }, + + deltacast: { + // Unused stub — deltacast capture uses sourceType='deltacast' path in + // _buildInputArgs, not the sourceBackends map. + buildInput() { + throw new Error('deltacast: use sourceType="deltacast" not sourceBackend'); + }, + }, + aja: { + buildInput() { + throw new Error('aja backend not yet implemented — requires hardware'); + }, + }, +}; + function buildEncodeArgs({ codec, videoBitrate, framerate, audioCodec, audioBitrate, audioChannels, container, isNetwork, isProxy = false, + growing = false, }) { + // NOTE: the growing master is NOT muxed by ffmpeg any more — raw2bmx writes + // the growing OP1a MXF from elementary essence FIFOs (see start()). The + // growing ffmpeg command (elementary MPEG-2 422 video + PCM audio to FIFOs, + // plus the HLS preview) is constructed directly in start(), so buildEncodeArgs + // is no longer called with growing=true. The `growing` param is retained for + // call-site compatibility; if ever set, fall through to the finalized path so + // we never silently produce a wrong file. + const v = VIDEO_CODECS[codec] || (isProxy ? VIDEO_CODECS.h264 : VIDEO_CODECS.prores_hq); const a = AUDIO_CODECS[audioCodec] || (isProxy ? AUDIO_CODECS.aac : AUDIO_CODECS.pcm_s24le); const fmt = CONTAINER_FMT[container] || (isProxy ? 'mp4' : 'mov'); @@ -164,9 +482,20 @@ function buildEncodeArgs({ if (a.bitrateControl && audioBitrate) args.push('-b:a', audioBitrate); if (audioChannels) args.push('-ac', String(audioChannels)); + // moov-atom placement is the difference between a Premiere-openable master and + // a "file cannot be opened" error. + // + // Finalized masters (the S3-piped recording that stops cleanly) must NOT be + // fragmented. Adobe Premiere's QuickTime/MOV importer reads the classic + // stco/stsz/stts sample tables in a single top-level moov; a fragmented MOV + // (moof/trun, empty sample tables) makes Premiere report "file cannot be + // opened." We write a clean, non-fragmented MOV instead. `+faststart` puts the + // moov before mdat on the second pass so the file is instantly + // seekable/streamable too. if (fmt === 'mov' || fmt === 'mp4') { - args.push('-movflags', '+frag_keyframe+empty_moov'); + args.push('-movflags', '+faststart'); } + // ProRes-in-MOV must carry a QuickTime brand or some importers reject the tag. args.push('-f', fmt); return args; @@ -191,7 +520,7 @@ class CaptureManager { * Returns { inputArgs, isNetwork } * @private */ - async _buildInputArgs({ sourceType, device, sourceUrl, listen, listenPort, streamKey }) { + async _buildInputArgs({ sourceType, sourceBackend = 'blackmagic', device, port, board, sourceUrl, listen, listenPort, streamKey }) { if (sourceType === 'srt') { let url; if (listen) { @@ -218,87 +547,280 @@ class CaptureManager { return { inputArgs: ['-probesize','32M','-analyzeduration','10M','-fflags','+genpts','-i', sourceUrl], isNetwork: true }; } - // Deltacast SDI via VideoMaster SDK FFmpeg plugin. - // FFmpeg input format is 'deltacast', device address is 'deltacast://'. - // When the physical device is absent (/dev/deltacast missing), fall back - // to a lavfi test card so development and integration testing work without hardware. + // Deltacast SDI via shared bridge daemon (deltacast-bridge). + // + // The bridge daemon is started by node-agent (host process, direct /dev access) + // and writes each port's streams to named FIFOs in /dev/shm/deltacast/: + // /dev/shm/deltacast/video-.fifo + // /dev/shm/deltacast/audio-.fifo + // + // This sidecar just reads from those FIFOs. The bridge may still be starting + // up or waiting for signal lock, so we wait up to 30s for the FIFOs to appear + // before handing them to ffmpeg. The bridge process is managed by node-agent; + // bridgeProcess is null here (no per-sidecar bridge spawn). if (sourceType === 'deltacast') { - const idx = (typeof device === 'number' || /^\d+$/.test(String(device))) - ? parseInt(device, 10) - : 0; - const { existsSync } = await import('node:fs'); - const deviceNode = `/dev/deltacast${idx}`; - if (existsSync(deviceNode)) { - console.log(`[capture] Deltacast index ${idx} → ${deviceNode} (hardware)`); - return { - inputArgs: ['-f', 'deltacast', '-i', `deltacast://${idx}`], - isNetwork: false, - }; - } else { - // No hardware — lavfi test card with port label + timecode burn-in. - // Matches the deltacast-sdi-recorder standalone app fallback exactly so - // recorded files look right in the MAM library during dev. - console.warn(`[capture] Deltacast device ${deviceNode} not found — using lavfi test card for port ${idx}`); - const testSrc = [ - `testsrc2=size=1920x1080:rate=30`, - `drawtext=text='DELTACAST PORT ${idx} — TEST MODE':fontsize=48:fontcolor=white:x=(w-text_w)/2:y=(h-text_h)/2`, - `drawtext=text='%{localtime\\:%H\\:%M\\:%S}':fontsize=32:fontcolor=yellow:x=10:y=10`, - ].join(','); - return { - inputArgs: [ - '-f', 'lavfi', '-i', testSrc, - '-f', 'lavfi', '-i', 'sine=frequency=1000:sample_rate=48000', - '-map', '0:v:0', '-map', '1:a:0', - ], - isNetwork: false, - }; + const idx = (typeof device === 'number' || /^\d+$/.test(String(device))) + ? parseInt(device, 10) : 0; + const portIdx = (typeof port === 'number' || /^\d+$/.test(String(port))) + ? parseInt(port, 10) : idx; + + const DC_PIPE_DIR = process.env.DELTACAST_PIPE_DIR || '/dev/shm/deltacast'; + const videoFifo = `${DC_PIPE_DIR}/video-${portIdx}.fifo`; + const audioFifo = `${DC_PIPE_DIR}/audio-${portIdx}.fifo`; + + // Wait up to 30s for both FIFOs to exist (bridge starts asynchronously). + const { existsSync: _exists } = await import('node:fs'); + const WAIT_MS = 30_000; + const POLL_MS = 500; + const deadline = Date.now() + WAIT_MS; + let videoReady = false; + let audioReady = false; + while (Date.now() < deadline) { + videoReady = _exists(videoFifo); + audioReady = _exists(audioFifo); + if (videoReady && audioReady) break; + await new Promise(r => setTimeout(r, POLL_MS)); } + if (!videoReady || !audioReady) { + throw new Error( + `deltacast bridge FIFOs not ready after ${WAIT_MS / 1000}s ` + + `(video=${videoReady} audio=${audioReady}) — is deltacast-bridge running?` + ); + } + console.log(`[deltacast] port ${portIdx} FIFOs ready: ${videoFifo}, ${audioFifo}`); + + // Resolution/fps are not known until the FIFO reader connects and starts + // receiving frames. We use sensible defaults here; ffmpeg's rawvideo demuxer + // will accept whatever the bridge writes once the pipe opens. + // The bridge daemon has already detected the signal and set up streams, so + // the FIFO content is ready-to-read as soon as the reader connects. + // + // NOTE: The format JSON emitted by the bridge on signal lock goes to the + // node-agent (which launched the bridge), not to this sidecar. The sidecar + // therefore uses fixed rawvideo params here. If per-port format introspection + // is needed in future, the node-agent should expose the fmt JSON via an API + // and capture-manager can query it before building inputArgs. + // + // For now, both video dimensions and framerate come from the recorder's + // configured values (passed to start() as `framerate` and implicit in the + // codec args). The rawvideo input is -video_size / -framerate from env or + // recorder config; ffmpeg tolerates a small mismatch in rawvideo (it just + // reads N bytes per frame based on the declared size). + // + // DELTACAST_VIDEO_SIZE / DELTACAST_FRAMERATE: set by node-agent in the + // sidecar env based on the bridge's per-port format JSON, if desired. + const dcSize = process.env.DELTACAST_VIDEO_SIZE || '1920x1080'; + const dcFps = process.env.DELTACAST_FRAMERATE || '60000/1001'; + const dcInterlaced = process.env.DELTACAST_INTERLACED === '1'; + + return { + inputArgs: [ + // Both raw FIFOs are timestampless. ffmpeg opens input 0 (video) and + // input 1 (audio) at slightly different moments, so PTS-zeroing each + // stream's first byte would bake in a fixed A/V offset. Stamping each + // input by wall-clock ARRIVAL time aligns them by real time regardless + // of FIFO open order — the robust fix for the A/V start offset. + // Large thread_queue_size avoids "thread message queue blocking" on + // the high-bitrate raw video FIFO. + '-use_wallclock_as_timestamps', '1', + '-thread_queue_size', '512', + '-f', 'rawvideo', + '-pix_fmt', 'uyvy422', + '-video_size', dcSize, + '-framerate', dcFps, + '-i', videoFifo, + '-use_wallclock_as_timestamps', '1', + '-thread_queue_size', '512', + '-f', 's16le', + '-ar', '48000', + '-ac', '2', + '-i', audioFifo, + ], + isNetwork: false, + bridgeProcess: null, /* bridge is managed by node-agent, not this sidecar */ + audioFifo: null, /* no per-session FIFO to clean up on stop */ + interlaced: dcInterlaced, + }; } - // Default: SDI via DeckLink - // device may be an integer index (0-based) or a full device name string. - // FFmpeg 7.x DeckLink requires the full name (e.g. 'DeckLink Duo 2 (2)'). - // Map integer index -> name using ffmpeg -sources decklink at runtime. - // - // ffmpeg -sources decklink output format: - // Auto-detected sources for decklink: - // DeckLink Duo 2 - // DeckLink Duo 2 (2) - // Lines containing device names start with whitespace; the header line - // starts with a non-space character. Previous code used a v4l2-style - // hex-address regex that never matched DeckLink output → index 1+ always - // fell through to a wrong fallback, producing black output from port 2+. - let deckLinkName = String(device); - if (typeof device === 'number' || /^\d+$/.test(String(device))) { - const idx = parseInt(device, 10); - try { - const { execSync } = await import('child_process'); - const out = execSync('ffmpeg -hide_banner -sources decklink 2>&1', { encoding: 'utf-8', timeout: 5000 }); - const names = []; - for (const line of out.split('\n')) { - // DeckLink source lines: " 81:76669a80:00000000 [DeckLink Duo (1)] (none)" - const m = line.match(/^\s+[0-9a-f:]+\s+\[([^\]]+)\]/); - if (m) names.push(m[1]); - } - if (names[idx]) { - deckLinkName = names[idx]; - console.log(`[capture] DeckLink index ${idx} → "${deckLinkName}" (from ${names.length} detected: ${names.join(', ')})`); - } else { - // Fallback: cannot determine model name without enumeration. - // Log a warning — operator should check the detected device list. - console.warn(`[capture] DeckLink index ${idx} out of range (detected ${names.length} devices: ${names.join(', ')}). Falling back to index-only input — capture may fail.`); - deckLinkName = `DeckLink (${idx})`; - } - } catch (err) { - console.warn(`[capture] ffmpeg -sources decklink failed: ${err.message}. Using index ${device} directly.`); - // Pass the numeric index directly; some ffmpeg builds accept it. - deckLinkName = String(device); - } + // Default: SDI via a pluggable source backend (issue #168). The backend + // selection defaults to `blackmagic` (DeckLink) so existing SDI recorders + // behave exactly as before. Deltacast/AJA backends throw until their + // hardware/SDK plumbing lands. + const backend = sourceBackends[sourceBackend]; + if (!backend) { + throw new Error(`Unknown source backend "${sourceBackend}" — expected one of: ${Object.keys(sourceBackends).join(', ')}`); } - return { - inputArgs: ['-f', 'decklink', '-i', deckLinkName], - isNetwork: false, - }; + return await backend.buildInput({ device }); + } + + /** + * Build the bash orchestrator command for the GROWING master (raw2bmx). + * + * One ffmpeg reads the source once (DeckLink can't be opened twice) and writes + * THREE outputs: + * (a) MPEG-2 422 elementary VIDEO → video FIFO ─┐ raw2bmx -t op1a reads + * (b) PCM s16le AUDIO → audio FIFO ─┘ these and writes the + * growing OP1a MXF. + * (c) H.264 HLS preview (unchanged) — keeps the UI monitor live. + * + * FIFO orchestration (the tricky part — proven on the live capture node): + * raw2bmx opens its inputs lazily (video first, reads the header, THEN opens + * audio), while ffmpeg opens ALL its outputs up-front and blocks on the + * audio FIFO until a reader appears → classic open-order deadlock. We break + * it by having the parent shell PRIME both FIFOs read-write (non-blocking + * open) so neither child blocks on open. CRUCIAL: the children must NOT + * inherit a priming *writer* (it would keep the FIFO open and starve raw2bmx + * of EOF forever), so each child closes the priming FDs before exec. The + * parent holds the priming FDs (as a reader/writer) only until raw2bmx has + * opened BOTH FIFOs, then drops them — leaving ffmpeg as the SOLE writer, so + * when ffmpeg exits raw2bmx gets a clean EOF and finalizes the MXF footer. + * + * Stop/finalize: the orchestrator traps SIGINT/SIGTERM and forwards SIGINT to + * ffmpeg (clean stop → EOF to raw2bmx), then `wait`s for raw2bmx and exits + * with raw2bmx's status. The Node side spawns this with detached:true and, on + * stop(), signals it and AWAITS its exit — so the finalized, valid MXF is on + * the share before the promotion worker uploads it. + * + * Returns the argv for spawn('bash', argv). + */ + _buildGrowingOrchestrator({ inputArgs, videoBitrate, resolution, framerate, audioChannels, outPath, hlsDir, videoCodec, audioMap = '0:a:0?', interlaced = false }) { + const { rawFlag, frameRate, ffRate } = deriveGrowingRaster(resolution, framerate, interlaced ? 'i' : 'p'); + const vb = videoBitrate || GROWING_DEFAULT_BITRATE; + const ach = audioChannels ? Number(audioChannels) : 2; + + // ffmpeg argv (shell-quoted). One decklink read → yadif → split → 3 outputs. + const sh = (a) => "'" + String(a).replace(/'/g, `'\\''`) + "'"; + // `-y`: the FIFOs are pre-created by mkfifo, so ffmpeg must overwrite them + // without the interactive "File already exists. Overwrite? [y/N]" prompt + // (which would otherwise abort the video/audio outputs and produce nothing). + const ff = ['ffmpeg', '-y', '-hide_banner', '-loglevel', 'warning', '-stats']; + // SDI input is interlaced; yadif then split into the master + preview taps. + // When there's an HLS dir we split the decode into the master ([vhi]) and + // the H.264 preview ([vlo]); with no HLS dir, split=1 (master only) so no + // split output is ever left unconnected (deltacast growing master had no + // HLS dir, leaving [vlo] orphaned -> 'split output 1 (vlo) unconnected'). + const filterComplex = hlsDir + ? (interlaced ? '[0:v]yadif=mode=1:deint=1,split=2[vhi][vlo]' : '[0:v]split=2[vhi][vlo]') + : (interlaced ? '[0:v]yadif=mode=1:deint=1,split=1[vhi]' : '[0:v]split=1[vhi]'); + const ffArgs = [ + ...inputArgs, + '-filter_complex', filterComplex, + // (a) MPEG-2 422 elementary video → "$VF" + '-map', '[vhi]', + ...GROWING_VIDEO_ELEMENTARY_ARGS, + '-b:v', vb, '-minrate', vb, '-maxrate', vb, '-bufsize', vb, + '-r', ffRate, + '-f', 'mpeg2video', '@VF@', + // (b) PCM s16le audio → "$AF" + '-map', audioMap, + '-c:a', 'pcm_s16le', '-ar', '48000', '-ac', String(ach), + '-f', 's16le', '@AF@', + ]; + let ffHls = []; + if (hlsDir) { + ffHls = [ + // (c) H.264 HLS preview — GPU-gated, unchanged behaviour. + '-map', '[vlo]', '-map', audioMap, + ...buildHlsVideoArgs(videoCodec, framerate), + '-c:a', 'aac', '-b:a', '128k', '-ar', '44100', + '-f', 'hls', '-hls_time', '2', '-hls_list_size', '15', + '-hls_flags', 'delete_segments+append_list+omit_endlist', + '-hls_segment_filename', `${hlsDir}/seg-%05d.ts`, + `${hlsDir}/index.m3u8`, + ]; + } + // @VF@/@AF@ are placeholders for the FIFO path shell variables; emit them as + // unquoted "$VF"/"$AF" so the shell expands them, and shell-quote everything + // else. + const placeholder = (t) => (t === '@VF@' ? '"$VF"' : t === '@AF@' ? '"$AF"' : sh(t)); + const ffLine = [...ff, ...ffArgs, ...ffHls].map(placeholder).join(' '); + + // raw2bmx argv. Audio is de-interleaved by raw2bmx into mono PCM tracks + // (the standard MXF mapping); --part starts a new body partition + + // IndexTableSegment every GROWING_PART_INTERVAL_FRAMES frames. + // + // CLIP TYPE: rdd9 (SMPTE RDD-9 / "Sony MXF") — NOT plain op1a and NOT + // --avid-gf. This is the make-or-break choice for Adobe Premiere: + // * --avid-gf produces an *Avid OP-Atom* growing file. That flavour needs a + // companion AAF to register the clip and is only read live by Avid Media + // Composer — Premiere cannot open it as a growing file. (Confirmed via the + // bmx mailing list + Softron/Drastic edit-while-ingest docs.) So it is + // removed. + // * Premiere's documented edit-while-ingest path expects XDCAM essence + // (MPEG-2 422 Long GOP, which we emit) wrapped as RDD-9. raw2bmx's `rdd9` + // clip type emits exactly that structure. + // --index-follows: write the IndexTableSegment in the *same* partition as the + // essence it indexes (rather than a trailing index-only partition). This is + // what lets a reader that re-scans body partitions on refresh find an index + // covering the newly-written frames — required so Premiere can seek past its + // original frame map toward the record head. + // The header Duration still starts at -1 and is only finalised in the footer + // on stop, so the inline Python dur-patch below overwrites the header Duration + // fields with the live frame count every 3s (Premiere reads the header + // Duration on each refresh; without the patch it sees duration=N/A). + const bmx = [ + 'raw2bmx', '-t', 'rdd9', '-o', '"$OUT"', '-f', frameRate, + '--part', String(GROWING_PART_INTERVAL_FRAMES), + '--index-follows', + rawFlag, '"$VF"', + '-s', '48000', '-q', '16', '--audio-chan', String(ach), '--pcm', '"$AF"', + ]; + const bmxLine = bmx + .map((t) => (t.startsWith('"$') ? t : sh(t))) + .join(' '); + + // The orchestration script. `set -m` is intentionally NOT used; we manage + // children explicitly. Priming FDs 7/8; children close them before exec. + // PATCHPID: inline Python duration-patcher that runs alongside raw2bmx and + // patches the MXF header's Duration=-1 fields with the actual frame count + // every 3 seconds. Without this Premiere sees Duration=N/A even as the file + // grows, so the timeline never extends. The patcher reads the last body + // partition's IndexTableSegment (IndexStartPosition+IndexDuration) to get + // an exact frame count, then seeks back to the header Duration fields and + // overwrites them in-place. It is killed by the cleanup trap on exit. + const script = ` +set -u +VF=$(mktemp -u /tmp/grow_v.XXXXXX); AF=$(mktemp -u /tmp/grow_a.XXXXXX) +OUT=${sh(outPath)} +mkfifo "$VF" "$AF" +PATCHPID= +cleanup() { rm -f "$VF" "$AF"; [ -n "$PATCHPID" ] && kill "$PATCHPID" 2>/dev/null; } +trap cleanup EXIT +# Prime both FIFOs read-write (non-blocking) to break the open-order deadlock. +exec 7<>"$VF" 8<>"$AF" +# raw2bmx: close priming FDs (no stray writer) before exec so it sees real EOF. +( exec 7>&- 8>&-; exec ${bmxLine} ) & +BMXPID=$! +# ffmpeg: also closes priming FDs; it opens its own write ends. +( exec 7>&- 8>&-; exec ${ffLine} ) & +FFPID=$! +# Forward a clean stop to ffmpeg; raw2bmx then gets EOF and finalizes the footer. +stop() { kill -INT "$FFPID" 2>/dev/null; } +trap stop INT TERM +# Drop the parent priming FDs once raw2bmx has opened BOTH FIFOs, so ffmpeg is +# the sole writer (its EOF reaches raw2bmx). If raw2bmx dies early, bail. +for i in $(seq 1 200); do + kill -0 "$BMXPID" 2>/dev/null || break + n=$(ls -l /proc/$BMXPID/fd 2>/dev/null | grep -c -- "$VF\\|$AF") + [ "\${n:-0}" -ge 2 ] && break + sleep 0.1 +done +exec 7>&- 8>&- +# No header-duration patcher is needed. In this bmx v1.6 build, raw2bmx's rdd9 +# writer with --part maintains a live, correct header Duration as the file grows +# (verified on-node: ffprobe reads a growing duration mid-write, e.g. 2.04s of a +# 10s clip while still recording). A patcher (the earlier dur-patch.py) was a +# no-op here — it searched for Duration=-1, which rdd9 never writes — and opening +# the file r+b while raw2bmx appends over CIFS only adds concurrency risk. +PATCHPID= +# Wait for ffmpeg (source end), then for raw2bmx to finalize the footer. +wait "$FFPID"; FFRC=$? +wait "$BMXPID"; BMXRC=$? +echo "[grow] ffmpeg rc=$FFRC raw2bmx rc=$BMXRC out=$OUT" >&2 +exit "$BMXRC" +`; + return ['-c', script]; } /** @@ -313,7 +835,14 @@ class CaptureManager { binId, clipName, device, + // Deltacast: one board (index 0) with 8 channels. `port` selects the + // channel; `board` selects the physical board (default 0). + port, + board, sourceType = 'sdi', + // Source-backend selection for SDI capture (issue #168). Defaults to + // `blackmagic` (DeckLink) so existing recorders are unaffected. + sourceBackend = 'blackmagic', sourceUrl, listen = false, listenPort, @@ -341,13 +870,16 @@ class CaptureManager { throw new Error('Capture already in progress'); } - const sessionId = uuidv4(); - const hiresExt = CONTAINER_EXT[container] || 'mov'; - const proxyExt = CONTAINER_EXT[proxyContainer] || 'mp4'; - const hiresKey = `projects/${projectId}/masters/${clipName}.${hiresExt}`; + // Stop the idle confidence monitor BEFORE touching the FIFO. A second + // reader on the video FIFO halves the capture rate (~29 fps) and desyncs + // audio — so the monitor must fully release the FIFO before recording. + this.stopIdlePreview(); - // Growing-files: write master to the local SMB share instead of streaming - // to S3. Path is relative to the container's GROWING_PATH mount. + const sessionId = uuidv4(); + const proxyExt = CONTAINER_EXT[proxyContainer] || 'mp4'; + + // Growing-files: write master to the SMB share instead of streaming to S3. + // Path is relative to the container's GROWING_PATH mount. // // Approach A: if a CIFS source is configured, mount it now. A mount failure // is non-fatal — we fall back to S3 streaming so the recording is never @@ -356,9 +888,22 @@ class CaptureManager { if (growingActive && GROWING_SMB_MOUNT) { if (!mountGrowingShare()) growingActive = false; // fall back to S3 } + // Growing master is always MXF OP1a / XDCAM HD422 written by raw2bmx (the + // format Premiere reads while growing — see GROWING_VIDEO_ELEMENTARY_ARGS / + // _buildGrowingOrchestrator), regardless of the recorder's configured + // container — so it gets a .mxf extension, not the container's. const growingPath = growingActive - ? `${GROWING_PATH}/${projectId}/${clipName}.${hiresExt}` + ? `${GROWING_PATH}/${projectId}/${clipName}.${GROWING_EXT}` : null; + + // hiresKey MUST match the actual master format/destination: + // - growing active → the master is a growing OP1a MXF on the share; the + // promotion worker uploads it to this key, so it has the .mxf extension. + // (A stale .mov key here would make the proxy job download a nonexistent + // object → "unable to open the file on disk".) + // - growing fell back to S3 → the normal container extension. + const hiresExt = growingPath ? GROWING_EXT : (CONTAINER_EXT[container] || 'mov'); + const hiresKey = `projects/${projectId}/masters/${clipName}.${hiresExt}`; if (growingPath) { try { mkdirSync(dirname(growingPath), { recursive: true }); } catch (err) { console.error('[capture] could not create growing dir:', err.message); } @@ -377,11 +922,20 @@ class CaptureManager { const startedAt = new Date().toISOString(); - const { inputArgs, isNetwork } = await this._buildInputArgs({ - sourceType, device, sourceUrl, listen, listenPort, streamKey, + this._sessionIdForBridge = sessionId; + const { inputArgs, isNetwork, bridgeProcess = null, audioFifo = null, interlaced = false } = await this._buildInputArgs({ + sourceType, sourceBackend, device, port, board, sourceUrl, listen, listenPort, streamKey, }); - const hiresCodecArgs = buildEncodeArgs({ + // Audio input index: the deltacast shared bridge delivers video on input 0 + // (video FIFO) and audio on input 1 (audio FIFO), so audioMap is '1:a:0?'. + // DeckLink SDI and network sources carry audio inside input 0. + const audioMap = (sourceType === 'deltacast') ? '1:a:0?' : '0:a:0?'; + + // Non-growing master: ffmpeg muxes the finalized MOV directly. Growing + // master: raw2bmx muxes the OP1a from elementary FIFOs (handled below via + // the orchestrator), so we don't build ffmpeg codec args here for it. + const hiresCodecArgs = growingPath ? null : buildEncodeArgs({ codec: videoCodec, videoBitrate, framerate, audioCodec, audioBitrate, audioChannels, container, @@ -389,55 +943,117 @@ class CaptureManager { isProxy: false, }); - console.log('[capture] hires ffmpeg args:', hiresCodecArgs.join(' ')); + if (hiresCodecArgs) console.log('[capture] hires ffmpeg args:', hiresCodecArgs.join(' ')); - const sdiFilterArgs = (sourceType === 'sdi') ? ['-vf', 'yadif=mode=1:deint=1'] : []; + const isInterlacedSource = sourceType === 'sdi' || (sourceType === 'deltacast' && interlaced); + const sdiFilterArgs = isInterlacedSource ? ['-vf', 'yadif=mode=1:deint=1'] : []; - // When growing-files is on, write directly to the SMB share so Premier - // can mount and edit the live file. Promotion worker uploads to S3 on EOF. - // Otherwise, stream the master to S3 via stdout pipe (legacy behavior). - const hiresOutput = growingPath ? growingPath : 'pipe:1'; - const hiresStdio = growingPath ? ['ignore', 'ignore', 'pipe'] : ['ignore', 'pipe', 'pipe']; + // Master output destination (NON-growing path only). + // + // - Growing-files on → the growing OP1a MXF is written directly to the SMB + // share by raw2bmx (see the orchestrator below); ffmpeg only produces the + // elementary essence FIFOs + HLS preview. `localMasterPath`/`hiresOutput` + // are unused in this case (the master path is `growingPath`). + // + // - Growing-files off → ffmpeg writes the MOV master to a LOCAL SEEKABLE + // temp file, then we upload to S3 on stop. We must NOT pipe the MOV muxer + // to S3 directly: the MOV/MP4 muxer cannot write to a non-seekable pipe + // without `empty_moov`, and an empty_moov/fragmented MOV is exactly what + // makes Adobe Premiere report "file cannot be opened" (no classic + // stco/stsz sample tables — samples live in moof/trun). A seekable file + // lets ffmpeg write a single contiguous moov with full sample tables and + // `+faststart` moves it to the front, producing a Premiere-native master. + const localMasterPath = growingPath + ? null + : `/tmp/capture/${sessionId}.${hiresExt}`; + if (localMasterPath) { + try { mkdirSync(dirname(localMasterPath), { recursive: true }); } + catch (err) { console.error('[capture] could not create temp master dir:', err.message); } + } + const hiresOutput = localMasterPath; + // Deltacast reads from FIFOs (no stdin pipe needed). DeckLink pipes stdout. + const hiresStdio = ['ignore', 'ignore', 'pipe']; // For SDI we cannot open the DeckLink device a second time for a preview // tee, so the live HLS preview is produced as a SECOND OUTPUT of the hires // ffmpeg: one decklink read -> yadif -> split -> [ProRes/S3] + [H.264/HLS]. let sdiHlsDir = null; - let hiresArgs; - if (sourceType === 'sdi' && this._assetIdForHls) { + if ((sourceType === 'sdi' || sourceType === 'deltacast') && this._assetIdForHls) { const fsMod = await import('node:fs'); sdiHlsDir = '/live/' + this._assetIdForHls; try { fsMod.mkdirSync(sdiHlsDir, { recursive: true }); } catch (_) {} - hiresArgs = [ - ...inputArgs, - '-filter_complex', '[0:v]yadif=mode=1:deint=1,split=2[vhi][vlo]', - // Output 0 — ProRes master (S3 pipe or growing file) - '-map', '[vhi]', '-map', '0:a:0?', - ...hiresCodecArgs, - hiresOutput, - // Output 1 — low-latency H.264 HLS preview for the UI monitor - '-map', '[vlo]', '-map', '0:a:0?', - '-c:v', 'libx264', '-preset', 'veryfast', '-tune', 'zerolatency', - '-pix_fmt', 'yuv420p', '-b:v', '2M', '-g', '60', '-sc_threshold', '0', - '-c:a', 'aac', '-b:a', '128k', '-ar', '44100', - '-f', 'hls', '-hls_time', '2', '-hls_list_size', '15', - '-hls_flags', 'delete_segments+append_list+omit_endlist', - '-hls_segment_filename', sdiHlsDir + '/seg-%05d.ts', - sdiHlsDir + '/index.m3u8', - ]; - console.log('[HLS] SDI preview as 2nd output -> ' + sdiHlsDir); - } else { - hiresArgs = [ ...inputArgs, ...sdiFilterArgs, ...hiresCodecArgs, hiresOutput ]; } - const hiresProcess = spawn('ffmpeg', hiresArgs, { stdio: hiresStdio }); - - const hiresUpload = growingPath - ? Promise.resolve({ growingPath }) - : createUploadStream(S3_BUCKET, hiresKey, hiresProcess.stdout); + let hiresProcess; + if (growingPath) { + // ── GROWING master: raw2bmx orchestrator ────────────────────────── + // One ffmpeg (single SDI read) → MPEG-2 422 elementary + PCM to FIFOs + + // the H.264 HLS preview; raw2bmx muxes the growing OP1a MXF from the FIFOs. + // Spawned via bash so the FIFO priming / EOF / stop-forwarding logic (see + // _buildGrowingOrchestrator) runs as one supervised unit. detached:true so + // it leads its own process group and we can clean-stop the whole pipeline. + const orchArgs = this._buildGrowingOrchestrator({ + inputArgs, + videoBitrate, + // Recorder raster for the raw2bmx essence flag. recorders.js sets + // RECORDING_RESOLUTION (e.g. '1920x1080' / '1080i' / 'native'); when + // 'native'/absent, deriveGrowingRaster defaults to 1080i59.94. + resolution: process.env.RECORDING_RESOLUTION || null, + framerate, + audioChannels, + outPath: growingPath, + hlsDir: (sourceType === 'sdi' || sourceType === 'deltacast') ? sdiHlsDir : null, + videoCodec, + audioMap, + interlaced: isInterlacedSource, + }); + console.log('[capture] growing master via raw2bmx; orchestrator script length=' + orchArgs[1].length); + hiresProcess = spawn('bash', orchArgs, { stdio: ['ignore', 'ignore', 'pipe'], detached: true }); + } else { + // ── Finalized (non-growing) master: ffmpeg muxes the MOV directly ── + let hiresArgs; + if ((sourceType === 'sdi' || sourceType === 'deltacast') && this._assetIdForHls) { + const filterStr = isInterlacedSource + ? '[0:v]yadif=mode=1:deint=1,split=2[vhi][vlo]' + : '[0:v]split=2[vhi][vlo]'; + hiresArgs = [ + ...inputArgs, + '-filter_complex', filterStr, + // Output 0 — ProRes/MOV master (local temp, uploaded to S3 on stop) + '-map', '[vhi]', '-map', audioMap, + // Keep raw audio aligned to the video clock. The two raw FIFOs carry + // no timestamps; -af aresample=async lets ffmpeg stretch/squeeze audio + // to correct any tiny rate mismatch so A/V never drifts over a long + // take. Applies to this output's mapped audio stream. + '-af', 'aresample=async=1:min_hard_comp=0.100000:first_pts=0', + ...hiresCodecArgs, + hiresOutput, + // Output 1 — low-latency H.264 HLS preview for the UI monitor. + // GPU-encoded (h264_nvenc) when the GPU is attached to this sidecar, + // otherwise libx264 (issue #164). GOP is pinned to one IDR per HLS + // segment so segments start on keyframes (avoids black/flashing). + '-map', '[vlo]', '-map', audioMap, + ...buildHlsVideoArgs(videoCodec, framerate), + '-c:a', 'aac', '-b:a', '128k', '-ar', '44100', + '-f', 'hls', '-hls_time', '2', '-hls_list_size', '15', + '-hls_flags', 'delete_segments+append_list+omit_endlist', + '-hls_segment_filename', sdiHlsDir + '/seg-%05d.ts', + sdiHlsDir + '/index.m3u8', + ]; + console.log('[HLS] SDI preview as 2nd output -> ' + sdiHlsDir); + } else { + hiresArgs = [ ...inputArgs, ...sdiFilterArgs, ...hiresCodecArgs, hiresOutput ]; + } + hiresProcess = spawn('ffmpeg', hiresArgs, { stdio: hiresStdio }); + } + // Growing-files: nothing to upload here (promotion worker handles S3). + // Non-growing: the master is uploaded from the finalized local file in + // stop(), once ffmpeg has written the moov and exited cleanly — we can't + // upload while recording because the file isn't a valid MOV until finalize. + // bridgeProcess is null for deltacast (bridge managed by node-agent on the host). const processes = { hires: hiresProcess }; - const uploads = { hires: hiresUpload }; + const uploads = { hires: growingPath ? Promise.resolve({ growingPath }) : null }; // ── HLS tee for network sources (live preview in the UI) ────────── let hlsProcess = null; @@ -450,8 +1066,8 @@ class CaptureManager { const hlsArgs = [ ...inputArgs, '-map', '0:v:0?', '-map', '0:a:0?', - '-c:v', 'libx264', '-preset', 'veryfast', '-tune', 'zerolatency', - '-pix_fmt', 'yuv420p', '-b:v', '2M', '-g', '60', '-sc_threshold', '0', + // GPU-gated preview encode, same as the SDI 2nd-output path (#164). + ...buildHlsVideoArgs(videoCodec, framerate), '-c:a', 'aac', '-b:a', '128k', '-ar', '44100', '-f', 'hls', '-hls_time', '2', '-hls_list_size', '15', '-hls_flags', 'delete_segments+append_list+omit_endlist', @@ -474,8 +1090,13 @@ class CaptureManager { const m = text.match(/frame=\s*(\d+)\s+fps=\s*([\d.]+)/); if (m) { this.state.framesReceived = parseInt(m[1], 10); - this.state.currentFps = parseFloat(m[2]); this.state.lastFrameAt = new Date().toISOString(); + if (this.state.recordingStartedAt) { + const elapsedSec = (Date.now() - this.state.recordingStartedAt) / 1000; + if (elapsedSec > 0) { + this.state.currentFps = Math.round((this.state.framesReceived / elapsedSec) * 100) / 100; + } + } } if (/Connection refused|No route to host|Connection failed|Input\/output error|Server returned|404 Not Found|Connection timed out/i.test(text)) { this.state.lastError = text.trim().slice(0, 240); @@ -492,6 +1113,7 @@ class CaptureManager { this.state.currentFps = 0; this.state.lastFrameAt = null; this.state.lastError = null; + this.state.recordingStartedAt = Date.now(); this.state.currentSession = { sessionId, projectId, @@ -500,9 +1122,12 @@ class CaptureManager { device, sourceType, sourceUrl, + assetId, hiresKey, proxyKey, growingPath, + localMasterPath, + audioFifo, startedAt, duration: 0, uploads, @@ -514,19 +1139,141 @@ class CaptureManager { }, }; + // Fire-and-forget: grab the first frame for the live poster thumbnail. + // Only for sources that produce an HLS dir (sdi/deltacast); never blocks start(). + if (sdiHlsDir && assetId) { + this._publishLiveThumbnail({ assetId, hlsDir: sdiHlsDir }).catch(() => {}); + } + return this._formatSessionResponse(); } + // ── Idle confidence monitor ──────────────────────────────────────────── + // A low-rate (1 fps) single-JPEG confidence snapshot for the recorder tile + // when the recorder is NOT actively recording. + // + // CRITICAL: this must NEVER read the video FIFO while a recording is active. + // A second continuous reader on the same /dev/shm/deltacast/video-N.fifo + // splits the frames between the two readers, halving the capture rate to + // ~29 fps (the root cause of the out-of-sync / fast-playback bug). So the + // monitor: + // 1. runs ONLY when this.state.recording === false + // 2. opens the FIFO, grabs ONE frame, scales to a small JPEG, exits + // 3. sleeps 1s, repeats — yielding the FIFO completely between grabs + // 4. is fully stopped the instant a recording starts (see start()) + async startIdlePreview() { + if (this._previewTimer || this._previewProc) return; // already running + if (this.state.recording) return; // never run during an active recording + const sourceType = process.env.SOURCE_TYPE; + const recorderId = process.env.RECORDER_ID; + if (!recorderId || !['deltacast', 'sdi'].includes(sourceType)) return; + if (sourceType !== 'deltacast') return; // SDI/blackmagic snapshot TBD + + const previewDir = `/live/preview-${recorderId}`; + try { await fs.promises.mkdir(previewDir, { recursive: true }); } catch (_) {} + + const size = process.env.DELTACAST_VIDEO_SIZE || '1920x1080'; + let cfg = {}; + try { cfg = JSON.parse(process.env.SOURCE_CONFIG || '{}'); } catch (_) {} + const port = cfg.port ?? 0; + const videoFifo = (process.env.DELTACAST_PIPE_DIR || '/dev/shm/deltacast') + `/video-${port}.fifo`; + const outJpg = previewDir + '/frame.jpg'; + const tmpJpg = previewDir + '/frame.tmp.jpg'; + + this._previewStop = false; + console.log('[preview] starting 1fps confidence monitor for', recorderId); + + const grabOnce = () => new Promise((resolve) => { + // Never compete with an active recording. + if (this._previewStop || this.state.recording) return resolve(); + // -frames:v 1 reads exactly ONE frame then exits, releasing the FIFO. + // Read-rate is capped by -readrate 1 so the single-frame read consumes + // ~1 frame worth of FIFO data, not a burst. + const ff = spawn('ffmpeg', [ + '-y', + '-f', 'rawvideo', '-pix_fmt', 'uyvy422', '-s', size, + '-i', videoFifo, + '-frames:v', '1', + '-vf', 'scale=480:-2', + '-q:v', '5', + tmpJpg, + ], { stdio: ['ignore', 'ignore', 'ignore'] }); + this._previewProc = ff; + const killTimer = setTimeout(() => { try { ff.kill('SIGKILL'); } catch (_) {} }, 4000); + ff.on('exit', () => { + clearTimeout(killTimer); + this._previewProc = null; + // Atomic-ish swap so the served frame is never half-written. + fs.rename(tmpJpg, outJpg, () => resolve()); + }); + ff.on('error', () => { clearTimeout(killTimer); this._previewProc = null; resolve(); }); + }); + + const loop = async () => { + while (!this._previewStop) { + await grabOnce(); + if (this._previewStop) break; + await new Promise(r => { this._previewTimer = setTimeout(r, 1000); }); + } + }; + loop(); + } + + stopIdlePreview() { + this._previewStop = true; + if (this._previewTimer) { clearTimeout(this._previewTimer); this._previewTimer = null; } + if (this._previewProc) { + try { this._previewProc.kill('SIGKILL'); } catch (_) {} + this._previewProc = null; + } + } + async stop(sessionId) { if (!this.state.recording || this.state.sessionId !== sessionId) { throw new Error('No active capture session or session ID mismatch'); } + this.stopIdlePreview(); + const { processes, currentSession } = this.state; + const isGrowing = !!currentSession.growingPath; + + // Send SIGINT and WAIT for the master writer to exit cleanly. + // - Non-growing: SIGINT flushes ffmpeg's MOV trailer (the moov atom with + // full sample tables). Uploading before finalize → "moov atom not found". + // - Growing: `processes.hires` is the bash ORCHESTRATOR (detached group + // leader). SIGINT hits its trap, which forwards SIGINT to ffmpeg; ffmpeg + // stops → raw2bmx gets EOF → raw2bmx writes the OP1a FOOTER and exits; + // only then does the orchestrator exit. Awaiting it guarantees the + // finalized, valid MXF is on the share before the promotion worker + // uploads it. raw2bmx footer finalize of a long recording can take longer + // than a MOV trailer flush, so the growing safety-net is more generous. + const finalizeTimeoutMs = isGrowing ? 60000 : 15000; + const waitExit = (proc) => new Promise((resolve) => { + if (!proc || proc.exitCode !== null || proc.signalCode !== null) return resolve(); + let done = false; + const finish = () => { if (!done) { done = true; resolve(); } }; + proc.once('exit', finish); + // Safety net: don't hang stop() forever if the writer refuses to exit. + setTimeout(() => { + try { + // Detached orchestrator → kill the whole process group (ffmpeg + + // raw2bmx + bash); otherwise just the process. + if (isGrowing && proc.pid) { try { process.kill(-proc.pid, 'SIGKILL'); } catch (_) {} } + proc.kill('SIGKILL'); + } catch (_) {} + finish(); + }, finalizeTimeoutMs); + }); + if (processes.hires) processes.hires.kill('SIGINT'); if (processes.proxy) processes.proxy.kill('SIGINT'); if (processes.hls) { try { processes.hls.kill('SIGINT'); } catch (_) {} } + /* processes.bridge: removed — bridge is managed by node-agent, not per-session */ + + // Wait for the master writer to finalize before we read/upload the file. + await waitExit(processes.hires); // Release the CIFS mount (best-effort) once the ffmpeg writers are done with // it. The promotion worker reads the staged file from the host/S3 side, not @@ -534,13 +1281,40 @@ class CaptureManager { unmountGrowingShare(); try { - const uploadPromises = [currentSession.uploads.hires]; + const uploadPromises = []; + + // Non-growing: upload the finalized local master file to S3 now that the + // moov has been written. Growing: the promotion worker handles S3. + if (currentSession.localMasterPath) { + let size = 0; + try { size = statSync(currentSession.localMasterPath).size; } catch (_) {} + if (size > 0) { + uploadPromises.push( + createUploadStream( + S3_BUCKET, + currentSession.hiresKey, + createReadStream(currentSession.localMasterPath), + ).then(() => { + try { unlinkSync(currentSession.localMasterPath); } catch (_) {} + }) + ); + } else { + console.warn('[capture] local master is 0 bytes — skipping upload:', currentSession.localMasterPath); + } + } else if (currentSession.uploads.hires) { + uploadPromises.push(currentSession.uploads.hires); + } + if (currentSession.uploads.proxy) uploadPromises.push(currentSession.uploads.proxy); await Promise.all(uploadPromises); } catch (error) { console.error('Error during upload completion:', error); } + if (currentSession.audioFifo) { + try { unlinkSync(currentSession.audioFifo); } catch (_) {} + } + const stoppedAt = new Date().toISOString(); const startTime = new Date(currentSession.startedAt); const stopTime = new Date(stoppedAt); @@ -549,6 +1323,7 @@ class CaptureManager { this.state.recording = false; this.state.sessionId = null; this.state.processes = {}; + this.state.recordingStartedAt = null; // No frames received → the upload (if any) produced a 0-byte object. // Surface that so the shutdown handler can mark the asset as 'error' @@ -557,6 +1332,7 @@ class CaptureManager { return { sessionId, + assetId: currentSession.assetId, projectId: currentSession.projectId, binId: currentSession.binId, clipName: currentSession.clipName, @@ -572,6 +1348,77 @@ class CaptureManager { }; } + // Grab the first video frame from the live HLS output and publish it as the + // asset's poster thumbnail, so the library shows a real frame instead of the + // "connecting…" placeholder while recording is still in progress. + // + // Runs entirely on the sidecar (where the HLS segments physically exist): + // 1. poll /live/ for the first seg-*.ts (bridge/ffmpeg warm-up) + // 2. ffmpeg -i -frames:v 1 -> scaled JPEG + // 3. upload JPEG to S3 at thumbnails/.jpg (matches mam-api convention) + // 4. POST /assets//live-thumbnail so the row gets thumbnail_s3_key + // + // Best-effort and non-blocking: any failure is logged and swallowed — the + // post-stop thumbnail job still produces the final thumbnail regardless. + async _publishLiveThumbnail({ assetId, hlsDir }) { + if (!assetId || !hlsDir) return; + const mamUrl = process.env.MAM_API_URL || 'http://mam-api:3000'; + const tmpJpg = `/tmp/livethumb-${assetId}.jpg`; + const thumbKey = `thumbnails/${assetId}.jpg`; + + try { + // 1. Wait up to 30s for the first HLS segment to appear. + const deadline = Date.now() + 30_000; + let segment = null; + while (Date.now() < deadline && this.state.recording && this.state.currentSession.assetId === assetId) { + try { + const entries = await fs.promises.readdir(hlsDir); + const segs = entries.filter(f => /^seg-\d+\.ts$/.test(f)).sort(); + if (segs.length > 0) { segment = `${hlsDir}/${segs[0]}`; break; } + } catch (_) { /* dir not created yet */ } + await new Promise(r => setTimeout(r, 500)); + } + if (!segment) { console.warn(`[livethumb] no segment for ${assetId} within 30s`); return; } + + // 2. Extract the first frame, scaled to 640px wide (yuvj420p for broad JPEG + // decoder compatibility), as a single still. + await new Promise((resolve, reject) => { + const ff = spawn('ffmpeg', [ + '-y', '-i', segment, + '-frames:v', '1', + '-vf', 'scale=640:-2', + '-pix_fmt', 'yuvj420p', + tmpJpg, + ], { stdio: ['ignore', 'ignore', 'pipe'] }); + let err = ''; + ff.stderr.on('data', d => { err += d.toString(); }); + ff.on('error', reject); + ff.on('exit', code => code === 0 ? resolve() : reject(new Error(`ffmpeg exit ${code}: ${err.slice(-200)}`))); + }); + + // 3. Upload to S3. + const size = statSync(tmpJpg).size; + if (size <= 0) throw new Error('extracted thumbnail is 0 bytes'); + await createUploadStream(S3_BUCKET, thumbKey, createReadStream(tmpJpg)); + + // 4. Tell mam-api the key (only sticks while the asset is still 'live'). + const resp = await fetch(`${mamUrl}/api/v1/assets/${assetId}/live-thumbnail`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(process.env.MAM_API_TOKEN ? { Authorization: `Bearer ${process.env.MAM_API_TOKEN}` } : {}), + }, + body: JSON.stringify({ thumbnailKey: thumbKey }), + }); + if (!resp.ok) throw new Error(`mam-api ${resp.status}: ${(await resp.text()).slice(0, 200)}`); + console.log(`[livethumb] published poster for ${assetId} (${thumbKey})`); + } catch (err) { + console.warn(`[livethumb] failed for ${assetId}:`, err.message); + } finally { + try { unlinkSync(tmpJpg); } catch (_) {} + } + } + getStatus() { if (!this.state.recording) return { recording: false }; @@ -625,4 +1472,4 @@ class CaptureManager { } export default new CaptureManager(); -export { VIDEO_CODECS, AUDIO_CODECS, CONTAINER_FMT, CONTAINER_EXT }; +export { VIDEO_CODECS, AUDIO_CODECS, CONTAINER_FMT, CONTAINER_EXT, sourceBackends }; diff --git a/services/capture/src/index.js b/services/capture/src/index.js index 59d0cbe..2d8e561 100644 --- a/services/capture/src/index.js +++ b/services/capture/src/index.js @@ -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 { diff --git a/services/capture/src/routes/capture.js b/services/capture/src/routes/capture.js index 158c224..3eb73f4 100644 --- a/services/capture/src/routes/capture.js +++ b/services/capture/src/routes/capture.js @@ -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); diff --git a/services/mam-api/src/db/migrations/031-cluster-nodes-last-seen.sql b/services/mam-api/src/db/migrations/031-cluster-nodes-last-seen.sql new file mode 100644 index 0000000..1dd0ab0 --- /dev/null +++ b/services/mam-api/src/db/migrations/031-cluster-nodes-last-seen.sql @@ -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; diff --git a/services/mam-api/src/db/migrations/032-recorder-gpu-affinity.sql b/services/mam-api/src/db/migrations/032-recorder-gpu-affinity.sql new file mode 100644 index 0000000..48173a3 --- /dev/null +++ b/services/mam-api/src/db/migrations/032-recorder-gpu-affinity.sql @@ -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; diff --git a/services/mam-api/src/db/migrations/033-playout-scte.sql b/services/mam-api/src/db/migrations/033-playout-scte.sql new file mode 100644 index 0000000..9d3e56d --- /dev/null +++ b/services/mam-api/src/db/migrations/033-playout-scte.sql @@ -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')); diff --git a/services/mam-api/src/db/migrations/034-add-pending-migration-status.sql b/services/mam-api/src/db/migrations/034-add-pending-migration-status.sql new file mode 100644 index 0000000..be76fc1 --- /dev/null +++ b/services/mam-api/src/db/migrations/034-add-pending-migration-status.sql @@ -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 +$$; diff --git a/services/mam-api/src/db/pool.js b/services/mam-api/src/db/pool.js index 9cdafff..380bdd0 100644 --- a/services/mam-api/src/db/pool.js +++ b/services/mam-api/src/db/pool.js @@ -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 diff --git a/services/mam-api/src/index.js b/services/mam-api/src/index.js index 8831034..59ffdb0 100644 --- a/services/mam-api/src/index.js +++ b/services/mam-api/src/index.js @@ -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'); diff --git a/services/mam-api/src/routes/assets.js b/services/mam-api/src/routes/assets.js index 81b20c7..8b657b0 100644 --- a/services/mam-api/src/routes/assets.js +++ b/services/mam-api/src/routes/assets.js @@ -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 `.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}`; diff --git a/services/mam-api/src/routes/cluster.js b/services/mam-api/src/routes/cluster.js index d13e64f..e50ee34 100644 --- a/services/mam-api/src/routes/cluster.js +++ b/services/mam-api/src/routes/cluster.js @@ -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 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); } diff --git a/services/mam-api/src/routes/playout.js b/services/mam-api/src/routes/playout.js index 168075e..ca66cc2 100644 --- a/services/mam-api/src/routes/playout.js +++ b/services/mam-api/src/routes/playout.js @@ -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//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 }; diff --git a/services/mam-api/src/routes/recorders.js b/services/mam-api/src/routes/recorders.js index 19e5868..48bba77 100644 --- a/services/mam-api/src/routes/recorders.js +++ b/services/mam-api/src/routes/recorders.js @@ -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=); + // 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 diff --git a/services/mam-api/src/routes/users.js b/services/mam-api/src/routes/users.js index b28bcb5..3e93dba 100644 --- a/services/mam-api/src/routes/users.js +++ b/services/mam-api/src/routes/users.js @@ -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:' 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; diff --git a/services/mam-api/src/scheduler.js b/services/mam-api/src/scheduler.js index e78fe41..6787dd9 100644 --- a/services/mam-api/src/scheduler.js +++ b/services/mam-api/src/scheduler.js @@ -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); } diff --git a/services/node-agent/index.js b/services/node-agent/index.js index f21c896..fc71219 100644 --- a/services/node-agent/index.js +++ b/services/node-agent/index.js @@ -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// 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 `. 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 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// + 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); diff --git a/services/playout/Dockerfile b/services/playout/Dockerfile index d82953f..5b29ac2 100644 --- a/services/playout/Dockerfile +++ b/services/playout/Dockerfile @@ -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 diff --git a/services/playout/entrypoint.sh b/services/playout/entrypoint.sh index 7cde7f1..7450abb 100644 --- a/services/playout/entrypoint.sh +++ b/services/playout/entrypoint.sh @@ -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 & diff --git a/services/playout/src/index.js b/services/playout/src/index.js index accbd2d..646f18e 100644 --- a/services/playout/src/index.js +++ b/services/playout/src/index.js @@ -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 diff --git a/services/playout/src/playout-manager.js b/services/playout/src/playout-manager.js index c846483..9c63512 100644 --- a/services/playout/src/playout-manager.js +++ b/services/playout/src/playout-manager.js @@ -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, }; } } diff --git a/services/premiere-plugin-uxp/dragonflight-mam-2.2.3.ccx b/services/premiere-plugin-uxp/dragonflight-mam-2.2.3.ccx new file mode 100644 index 0000000..80d4917 Binary files /dev/null and b/services/premiere-plugin-uxp/dragonflight-mam-2.2.3.ccx differ diff --git a/services/premiere-plugin-uxp/manifest.json b/services/premiere-plugin-uxp/manifest.json index 2a3d5ff..e8931da 100644 --- a/services/premiere-plugin-uxp/manifest.json +++ b/services/premiere-plugin-uxp/manifest.json @@ -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", diff --git a/services/premiere-plugin-uxp/src/import-flow.js b/services/premiere-plugin-uxp/src/import-flow.js index adfdc71..86fa151 100644 --- a/services/premiere-plugin-uxp/src/import-flow.js +++ b/services/premiere-plugin-uxp/src/import-flow.js @@ -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) { diff --git a/services/web-ui/public/app.jsx b/services/web-ui/public/app.jsx index 1bcf73b..fe67838 100644 --- a/services/web-ui/public/app.jsx +++ b/services/web-ui/public/app.jsx @@ -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(() => { diff --git a/services/web-ui/public/data.jsx b/services/web-ui/public/data.jsx index b940408..1888b14 100644 --- a/services/web-ui/public/data.jsx +++ b/services/web-ui/public/data.jsx @@ -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: [], diff --git a/services/web-ui/public/downloads/dragonflight-mam-2.2.3.ccx b/services/web-ui/public/downloads/dragonflight-mam-2.2.3.ccx new file mode 100644 index 0000000..8c3c548 Binary files /dev/null and b/services/web-ui/public/downloads/dragonflight-mam-2.2.3.ccx differ diff --git a/services/web-ui/public/modal-new-recorder.jsx b/services/web-ui/public/modal-new-recorder.jsx index e89900a..e3eeffe 100644 --- a/services/web-ui/public/modal-new-recorder.jsx +++ b/services/web-ui/public/modal-new-recorder.jsx @@ -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 }) {
{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). */} +
+ {[ + { 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 => ( + + ))} +
- - setRecCodec(e.target.value)} disabled={growingOn} + style={{ appearance: 'auto', opacity: growingOn ? 0.6 : 1 }}> + {growingOn && } + + + @@ -412,7 +448,7 @@ function NewRecorderModal({ open, onClose }) {
- {codecUsesBitrate ? ( + {showBitrate ? (
)} - - + + {/* #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; })()}
+ )} {recTab === 'audio' && (
@@ -455,8 +492,10 @@ function NewRecorderModal({ open, onClose }) { )} {recTab === 'container' && (
- - + +
)}
@@ -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.
+ {growingOn && ( +
+ 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. +
+ )}
diff --git a/services/web-ui/public/screens-admin.jsx b/services/web-ui/public/screens-admin.jsx index c561998..6523efb 100644 --- a/services/web-ui/public/screens-admin.jsx +++ b/services/web-ui/public/screens-admin.jsx @@ -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 (
+ {confirmModal}

Users & Groups

@@ -258,28 +274,7 @@ function Users() { {tab === 'groups' && } {tab === 'policies' && ( -
-
- -
Access model
-
-
-
- admin — full access to every - project plus user, group, cluster, and system administration. -
-
- editor / viewer — see only the - projects they've been granted. A view grant is read-only; an - edit grant allows changes. Grants can target an individual user or a group. -
-
- Manage a project's grants from the Projects page - → a project's Manage access… menu. Group membership is managed on the - Groups tab above. -
-
-
+ )}
{showInvite && 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 onChangeRole(u, e.target.value)} + className="field-input" + style={{ width: 90, padding: '3px 6px', fontSize: 11.5, appearance: 'auto' }}> + + + + +
+
+ {u.totp_enabled + ? 2FA on + : 2FA off} +
+
+ +
+
+ {u.totp_enabled && ( + + )} +
+
+ + {expanded && ( +
+ {loading &&
Loading access…
} + {accessErr &&
{accessErr}
} + {!loading && !accessErr && (u.role === 'admin') && ( +
+ + Admin: full access to every project. +
+ )} + {!loading && !accessErr && u.role !== 'admin' && ( +
+ {/* Accessible projects */} +
+
+ Projects ({projects.length}) +
+ {projects.length === 0 && ( +
No project access granted.
+ )} + {projects.map(p => { + // Backend `via` is 'direct' for a user grant, or 'group:' + // 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 ( +
+ {p.project_name || p.name || p.project_id || p.id} + {p.level || 'view'} + + {viaLabel} + +
+ ); + })} +
+ {/* Group memberships */} +
+
+ Groups ({memberships.length}) +
+ {memberships.length === 0 && ( +
Not a member of any group.
+ )} +
+ {memberships.map(g => ( + + {g.name || g.group_name || g.group_id} + + ))} +
+
+
+ )} +
+ )} + + ); +} + 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 (
+ {confirmModal}
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 (
+ {confirmModal}

Containers

Docker Compose services across the cluster @@ -976,7 +1175,11 @@ function Containers() { {(c.cpu || 0).toFixed(1)}%
-
{c.mem} MB
+
+ {c.memBytes != null + ? `${Math.round(c.memBytes / 1048576)} MB` + : "N/A"} +
{c.ports}
@@ -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 ( +
+
+ + Capture Drivers / SDKs + {status && status.kernel && ( + + kernel {status.kernel} + + )} +
+ + {statusErr && ( +
+ Driver status unavailable: {statusErr} +
+ )} + +
+ {DRIVER_VENDORS.map(v => { + const installed = isInstalled(v.key); + const installing = busy === v.key; + return ( +
+
+
{v.label}
+
{v.hint}
+
+ + {loading ? "…" : (installed ? "INSTALLED" : "NOT INSTALLED")} + + +
+ ); + })} +
+ + {log && ( +
+
+ {log.vendor} install output + {log.ok === true && SUCCESS} + {log.ok === false && FAILED} + {log.rebootRequired && ( + + ⚠ REBOOT REQUIRED + + )} +
+
{log.text}
+
+ )} +
+ ); +} + // 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 (
+ {confirmModal}

Cluster

{NODES.filter(n => n.status === "online").length} of {NODES.length} nodes online @@ -1365,6 +1712,23 @@ function Cluster() {
)} {g.device &&
{g.device}
} + {/* Issue #166 — live NVENC/GPU encode telemetry (0 until a live encode runs) */} + {(g.utilPct != null || g.encUtilPct != null || g.nvencSessions != null) && ( +
+ {g.utilPct != null && ( + GPU {g.utilPct}% + )} + {g.encUtilPct != null && ( + ENC 0 ? "var(--success)" : "var(--text-2)" }}>{g.encUtilPct}% + )} + {g.memUsedMb != null && g.memMb && ( + VRAM {g.memUsedMb}/{g.memMb} MB + )} + {g.nvencSessions != null && ( + NVENC 0 ? "var(--success)" : "var(--text-2)" }}>{g.nvencSessions} + )} +
+ )}
+ + {/* ── Capture Drivers / SDKs ── */} +
@@ -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)} />
-
- -
- {ADD_NODE_ROLES.map(rd => ( - - ))} -
-
-
- This token is shown only once — copy the command now. + This token is shown only once. Copy the command now.
{command} +
+ Profiles (worker / capture / GPU) are auto-selected from the new machine's detected hardware — no need to choose. +
  1. SSH into the fresh Ubuntu machine.
  2. Paste and run this command.
  3. @@ -1716,7 +2060,7 @@ function TotpSection() {
- 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:
{enroll.secret}
@@ -1920,7 +2264,7 @@ function StorageWarningBanner() { }}>
- 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.
@@ -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 (
+ {confirmModal}
{vendor.name} {deployed diff --git a/services/web-ui/public/screens-asset.jsx b/services/web-ui/public/screens-asset.jsx index a6beafb..12f35a3 100644 --- a/services/web-ui/public/screens-asset.jsx +++ b/services/web-ui/public/screens-asset.jsx @@ -23,6 +23,7 @@ function AssetDetail({ asset, onClose }) { const [comments, setComments] = React.useState([]); const [newComment, setNewComment] = React.useState(""); const [commentsLoading, setCommentsLoading] = React.useState(false); + const [confirm, confirmModal] = window.useConfirm(); // Stream / video state const [streamUrl, setStreamUrl] = React.useState(null); @@ -198,6 +199,45 @@ function AssetDetail({ asset, onClose }) { // Pull a presigned hi-res URL and trigger a browser download with the // asset's display name as the filename. Falls back to opening in a new tab. const [downloading, setDownloading] = React.useState(false); + + // Gate the download behind a one-time "large file / connection speed" + // warning, shared with the library via the df.lib.download.warnDismissed + // localStorage flag. Once dismissed, downloads start without the prompt. + const dismissForeverRef = React.useRef(false); + const requestDownload = async function() { + if (downloading) return; + let dismissed = false; + try { dismissed = localStorage.getItem('df.lib.download.warnDismissed') === '1'; } catch (_) {} + if (!dismissed) { + dismissForeverRef.current = false; + const ok = await confirm({ + title: 'Download original', + message:
+
+ You're about to download the full-length original master for {asset.name}. + These files can be very large and download speed depends on your connection. +
+ +
, + confirmLabel: 'Download', + cancelLabel: 'Cancel', + danger: false, + }); + if (!ok) return; + // Persist the dismissal only after the user confirms the download. + if (dismissForeverRef.current) { + try { localStorage.setItem('df.lib.download.warnDismissed', '1'); } catch (_) {} + } + } + downloadHires(); + }; + const downloadHires = function() { if (downloading) return; setDownloading(true); @@ -231,9 +271,12 @@ function AssetDetail({ asset, onClose }) { if (navigator.clipboard) navigator.clipboard.writeText(assetId).catch(function() {}); setMenuOpen(false); }; - const deleteAsset = function() { + const deleteAsset = async function() { setMenuOpen(false); - if (!window.confirm('Delete "' + (asset.name || assetId) + '" permanently?\nRemoves DB row + S3 objects. Cannot be undone.')) return; + if (!(await confirm({ + title: 'Delete asset?', + message: 'Delete "' + (asset.name || assetId) + '" permanently?\nRemoves DB row + S3 objects. Cannot be undone.', + }))) return; window.ZAMPP_API.fetch('/assets/' + assetId + '?hard=true', { method: 'DELETE' }) .then(function() { onClose && onClose(); }) .catch(function(e) { window.alert('Delete failed: ' + e.message); }); @@ -325,8 +368,8 @@ function AssetDetail({ asset, onClose }) { .catch(function() {}); }; - const deleteComment = function(c) { - if (!confirm('Delete this comment?')) return; + const deleteComment = async function(c) { + if (!(await confirm({ title: 'Delete comment?', message: 'Delete this comment?' }))) return; window.ZAMPP_API.fetch('/assets/' + assetId + '/comments/' + c.id, { method: 'DELETE' }) .then(function() { setComments(function(prev) { return prev.filter(x => x.id !== c.id); }); @@ -355,6 +398,7 @@ function AssetDetail({ asset, onClose }) { return (
+ {confirmModal}
@@ -367,9 +411,11 @@ function AssetDetail({ asset, onClose }) {
- + {asset.original_s3_key && ( + + )}
-
{tab === "comments" && (
diff --git a/services/web-ui/public/screens-auth.jsx b/services/web-ui/public/screens-auth.jsx index f3bbd1a..f1e3a0e 100644 --- a/services/web-ui/public/screens-auth.jsx +++ b/services/web-ui/public/screens-auth.jsx @@ -76,7 +76,7 @@ style={{ width: '100%', background: disabled ? 'var(--bg-3)' : 'var(--accent)', - color: '#fff', + color: disabled ? 'var(--text-3)' : '#0a0c10', border: 'none', borderRadius: 4, padding: '9px', @@ -210,7 +210,7 @@ // An expired/used ticket means the user must start over. if (r.status === 401 && /ticket/.test(body.error || '')) { setTicket(null); setCode(''); setPassword(''); - setError('Session timed out — please sign in again.'); + setError('Session timed out. Please sign in again.'); } else { setError(body.error || ('Verification failed: ' + r.status)); } diff --git a/services/web-ui/public/screens-editor.jsx b/services/web-ui/public/screens-editor.jsx index b490096..34b6d4e 100644 --- a/services/web-ui/public/screens-editor.jsx +++ b/services/web-ui/public/screens-editor.jsx @@ -7,6 +7,7 @@ function Editor() { const [projectId, setProjectId] = React.useState(null); const [sequences, setSequences] = React.useState([]); const [currentSeq, setCurrentSeq] = React.useState(null); + const [confirm, confirmModal] = window.useConfirm(); const [assets, setAssets] = React.useState([]); const [bins, setBins] = React.useState([]); const [sourceAsset, setSourceAsset] = React.useState(null); @@ -159,7 +160,7 @@ function Editor() { async function deleteSequence() { if (!currentSeq) return; - if (!window.confirm('Delete sequence "' + currentSeq.name + '"? This cannot be undone.')) return; + if (!(await confirm({ title: 'Delete sequence?', message: 'Delete sequence "' + currentSeq.name + '"? This cannot be undone.' }))) return; try { await window.ZAMPP_API.deleteSequence(currentSeq.id); const remaining = sequences.filter(s => s.id !== currentSeq.id); @@ -390,6 +391,7 @@ function Editor() { return (
+ {confirmModal} {/* Beta banner: flat strip across top, no glassmorphism or gradient. */}
diff --git a/services/web-ui/public/screens-home.jsx b/services/web-ui/public/screens-home.jsx index b96fc92..5a11986 100644 --- a/services/web-ui/public/screens-home.jsx +++ b/services/web-ui/public/screens-home.jsx @@ -23,12 +23,19 @@ function Home({ navigate }) { // Pull live counts so the tile subtitles ("34 assets", "0 live", "3 running") // reflect what's actually in the DB right now, not a stale boot-time cache. const [cards, setCards] = React.useState({}); + // Playout has no /metrics/home card yet (and the playout schema may not be + // migrated on every install); fetch /playout/channels separately and degrade + // silently — the tile just shows "No channels" if the endpoint isn't there. + const [playoutChannels, setPlayoutChannels] = React.useState(null); React.useEffect(() => { let cancelled = false; const load = () => { window.ZAMPP_API.fetch('/metrics/home?hours=1') .then(d => { if (!cancelled) setCards(d?.cards || {}); }) .catch(() => {}); + window.ZAMPP_API.fetch('/playout/channels') + .then(d => { if (!cancelled) setPlayoutChannels(Array.isArray(d) ? d : []); }) + .catch(() => { if (!cancelled) setPlayoutChannels([]); }); }; load(); const t = setInterval(load, 30_000); @@ -67,17 +74,24 @@ function Home({ navigate }) { id: 'playout', label: 'Playout', icon: 'signal', - tone: 'live', - sub: 'Master Control', - desc: 'Play assets to SDI, NDI, SRT or RTMP via CasparCG.', + tone: 'accent', + sub: (() => { + if (playoutChannels === null) return '·'; + const total = playoutChannels.length; + const onAir = playoutChannels.filter(c => c.status === 'running').length; + if (total === 0) return 'No channels'; + if (onAir > 0) return onAir + ' on air · ' + total + ' channel' + (total === 1 ? '' : 's'); + return total + ' channel' + (total === 1 ? '' : 's'); + })(), + desc: 'Master Control. SDI · NDI · SRT · RTMP playout, playlists, as-run.', }, { id: '__downloads', label: 'Downloads', icon: 'download', tone: 'purple', - sub: 'Premiere panel · Dragon-ISO', - desc: 'Download the Premiere Pro panel and Dragon-ISO NDI tools.', + sub: 'Plugin · Teams ISO', + desc: 'Download the Premiere Pro UXP plugin and the Teams ISO installer.', }, { id: 'jobs', @@ -270,14 +284,20 @@ function Home({ navigate }) { )}
+ +
Created by Wild Dragon LLC
{showDownloads && setShowDownloads(false)} />}
); } -// Combined downloads modal: Premiere Pro panel + Dragon-ISO. +// Modal listing all downloads: the Premiere Pro UXP plugin (.ccx, one per +// released version, sourced from window.PREMIERE_RELEASES written by the +// Settings → SDKs section in screens-admin.jsx) plus the Teams ISO installer +// (window.TEAMS_ISO; the .exe slot is wired but the file may still be pending). function DownloadsModal({ onClose }) { + const teamsIso = window.TEAMS_ISO || {}; const releases = (window.PREMIERE_RELEASES || []).slice().sort((a, b) => { const av = String(a.version || ''), bv = String(b.version || ''); return bv.localeCompare(av, undefined, { numeric: true }); @@ -293,20 +313,37 @@ function DownloadsModal({ onClose }) {
Downloads
- Premiere Pro panel and Dragon-ISO NDI tools. + The Premiere Pro (UXP) plugin and the Teams ISO installer. Install the .ccx per workstation via the Adobe UXP Developer Tool, or double-click it with Creative Cloud installed.
- {/* ── Premiere panel ── */} -
- - Premiere Pro panel (UXP) -
-
- Install the .ccx per workstation via the Adobe UXP Developer Tool, or double-click it with Creative Cloud installed. +
+
+ Teams ISO + {teamsIso.version && ( + v{teamsIso.version} + )} +
+
+ Windows installer for the Teams ISO workstation build. +
+
+ {teamsIso.available && teamsIso.url ? ( + + Teams ISO (.exe) + + ) : ( + <> + + Teams ISO (.exe) + + coming soon, file pending + + )} +
{releases.length === 0 && (
@@ -503,7 +540,7 @@ function Dashboard({ navigate }) { const alerts = [ ...erroredRecorders.map(r => ({ key: 'r-' + r.id, sev: 'danger', - title: r.name + ' — recorder error', + title: r.name + ': recorder error', meta: r.error_message || r.url || 'signal lost', action: 'Reconnect', to: 'recorders', })), @@ -758,7 +795,7 @@ function OnAirEmpty({ sources, onStart }) {
Nothing on air
-
All recorders are idle — start a source to begin capturing.
+
All recorders are idle. Start a source to begin capturing.
diff --git a/services/web-ui/public/screens-ingest.jsx b/services/web-ui/public/screens-ingest.jsx index 9da9262..9f7df4a 100644 --- a/services/web-ui/public/screens-ingest.jsx +++ b/services/web-ui/public/screens-ingest.jsx @@ -489,24 +489,143 @@ function HlsPreview({ assetId, recorderId, muted = true, controls = false, class ); } +/* ===== Idle confidence monitor — 1 fps JPEG snapshot ===== */ +/* Refreshes a single JPEG once per second. Does NOT open the video FIFO as a + * continuous reader, so it never competes with / slows down an active capture. */ +function JpegSnapshotPreview({ url }) { + const [src, setSrc] = React.useState(null); + const [failed, setFailed] = React.useState(false); + + React.useEffect(() => { + if (!url) return; + let destroyed = false; + let timer = 0; + const tick = () => { + if (destroyed) return; + const bust = url + (url.includes('?') ? '&' : '?') + 't=' + Date.now(); + const img = new Image(); + img.onload = () => { if (!destroyed) { setSrc(bust); setFailed(false); } }; + img.onerror = () => { if (!destroyed) setFailed(true); }; + img.src = bust; + timer = setTimeout(tick, 1000); + }; + tick(); + return () => { destroyed = true; if (timer) clearTimeout(timer); }; + }, [url]); + + return ( +
+ {src && ( + signal + )} + {(!src || failed) && ( +
+ {failed ? 'no signal' : 'connecting…'} +
+ )} +
+ ); +} + +/* ===== Idle signal preview (always-on HLS from sidecar) ===== */ +function HlsPreviewUrl({ url }) { + const videoRef = React.useRef(null); + const [err, setErr] = React.useState(null); + + React.useEffect(() => { + if (!url || !videoRef.current) return; + const v = videoRef.current; + let destroyed = false; + let hls = null; + let retryTimer = 0; + let retryCount = 0; + + const clearRetry = () => { if (retryTimer) { clearTimeout(retryTimer); retryTimer = 0; } }; + + if (v.canPlayType('application/vnd.apple.mpegurl')) { + const tryLoad = () => { + if (destroyed) return; + v.src = url; + v.play().catch(() => {}); + }; + v.addEventListener('error', () => { + retryCount++; + clearRetry(); + retryTimer = setTimeout(tryLoad, Math.min(2000 * retryCount, 15000)); + setErr('no signal'); + }); + v.addEventListener('playing', () => { retryCount = 0; setErr(null); }, { once: false }); + tryLoad(); + return () => { destroyed = true; clearRetry(); }; + } + + if (!window.Hls) { setErr('hls.js missing'); return; } + + const startHls = () => { + if (destroyed) return; + hls = new window.Hls({ liveSyncDurationCount: 2, lowLatencyMode: true }); + hls.loadSource(url); + hls.attachMedia(v); + hls.on(window.Hls.Events.ERROR, (_e, data) => { + if (data.fatal) { + try { hls.destroy(); } catch (_) {} + hls = null; + retryCount++; + setErr('no signal'); + clearRetry(); + retryTimer = setTimeout(startHls, Math.min(2000 * retryCount, 15000)); + } + }); + hls.on(window.Hls.Events.MANIFEST_PARSED, () => { retryCount = 0; setErr(null); }); + }; + + startHls(); + v.play().catch(() => {}); + return () => { destroyed = true; clearRetry(); if (hls) { try { hls.destroy(); } catch (_) {} } }; + }, [url]); + + return ( +
+
+ ); +} + /* ===== Recorders ===== */ function _normRecorder(r) { - let elapsed = '·'; - if (r.status === 'recording' && r.started_at) { - const s = Math.floor((Date.now() - new Date(r.started_at)) / 1000); - elapsed = String(Math.floor(s / 3600)).padStart(2, '0') + ':' + - String(Math.floor((s % 3600) / 60)).padStart(2, '00') + ':' + - String(s % 60).padStart(2, '0'); - } const cfg = r.source_config || {}; + // Surface the capture port for SDI / Deltacast recorders so the recorder card + // can show which physical input the recorder is bound to. For Deltacast, + // cfg.port is the bridge port index (0-7). For Blackmagic SDI, cfg.device + // is something like /dev/blackmagic/dv0 — we slice off the trailing index. + let capturePort = null; + if (r.source_type === 'deltacast') { + capturePort = cfg.port != null ? `Port ${cfg.port}` : null; + } else if (r.source_type === 'sdi') { + const dev = cfg.device || ''; + const m = dev.match(/(\d+)$/); + if (m) capturePort = `SDI ${m[1]}`; + } return { ...r, source: r.source_type || '·', url: cfg.url || cfg.address || cfg.srt_url || cfg.rtmp_url || r.source_type || '·', codec: r.recording_codec || '·', res: r.recording_resolution || '·', + framerate: r.recording_framerate || 'native', node: r.node_id ? r.node_id.slice(0, 8) : 'primary', - elapsed, + capturePort, + previewUrl: r.preview_url || null, + elapsed: '·', bitrate: '·', health: 100, audio: false, @@ -587,6 +706,7 @@ function RecorderRow({ recorder: initialRecorder, onRefresh }) { const [clipName, setClipName] = React.useState(''); // Project override for this take. Defaults to the recorder's configured project. const [takeProjectId, setTakeProjectId] = React.useState(initialRecorder.project_id || PROJECTS[0]?.id || ''); + const [confirm, confirmModal] = window.useConfirm(); const isRec = recorder.status === 'recording'; // Keep takeProjectId in sync if the recorder row changes (e.g. after a refresh). @@ -609,15 +729,43 @@ function RecorderRow({ recorder: initialRecorder, onRefresh }) { return () => clearInterval(id); }, [isRec, recorder.id]); + // Tick elapsed every second while recording. Seed from liveStatus.duration + // (authoritative from the capture container) when available; fall back to + // wall-clock diff from recorder.started_at so the counter never freezes. + const [elapsedSecs, setElapsedSecs] = React.useState(0); + React.useEffect(() => { + if (!isRec) { setElapsedSecs(0); return; } + const base = () => { + if (liveStatus && liveStatus.duration != null) return liveStatus.duration; + if (recorder.started_at) return Math.floor((Date.now() - new Date(recorder.started_at).getTime()) / 1000); + return 0; + }; + // Snap to latest authoritative value immediately, then tick from there. + const anchor = { at: Date.now(), secs: base() }; + setElapsedSecs(anchor.secs); + const id = setInterval(() => { + setElapsedSecs(anchor.secs + Math.floor((Date.now() - anchor.at) / 1000)); + }, 1000); + return () => clearInterval(id); + // Re-anchor whenever liveStatus.duration arrives from the poll. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isRec, liveStatus && liveStatus.duration, recorder.started_at]); + const displayElapsed = React.useMemo(() => { - if (liveStatus && liveStatus.duration != null) { - const d = Math.max(0, liveStatus.duration); - return String(Math.floor(d / 3600)).padStart(2, '0') + ':' + - String(Math.floor((d % 3600) / 60)).padStart(2, '0') + ':' + - String(d % 60).padStart(2, '0'); + if (!isRec) return '·'; + const d = Math.max(0, elapsedSecs); + return String(Math.floor(d / 3600)).padStart(2, '0') + ':' + + String(Math.floor((d % 3600) / 60)).padStart(2, '0') + ':' + + String(d % 60).padStart(2, '0'); + }, [isRec, elapsedSecs]); + + // Show live fps when recording and signal is healthy; fall back to configured value. + const displayFramerate = React.useMemo(() => { + if (isRec && liveStatus && liveStatus.currentFps != null && liveStatus.currentFps > 0) { + return Number(liveStatus.currentFps).toFixed(2) + ' fps'; } - return recorder.elapsed; - }, [liveStatus, recorder.elapsed]); + return recorder.framerate || 'native'; + }, [isRec, liveStatus, recorder.framerate]); const displaySignal = liveStatus ? (liveStatus.signal || '·') @@ -657,8 +805,8 @@ function RecorderRow({ recorder: initialRecorder, onRefresh }) { .catch(e => { setPending(false); setErr(e.message || 'Failed'); setRecorder(initialRecorder); }); }; - const handleDelete = () => { - if (!window.confirm('Delete recorder "' + recorder.name + '"?\nThis will stop any active recording and cannot be undone.')) return; + const handleDelete = async () => { + if (!(await confirm({ title: 'Delete recorder?', message: 'Delete recorder "' + recorder.name + '"?\nThis will stop any active recording and cannot be undone.' }))) return; window.ZAMPP_API.fetch('/recorders/' + recorder.id, { method: 'DELETE' }) .then(() => { onRefresh(); @@ -670,6 +818,7 @@ function RecorderRow({ recorder: initialRecorder, onRefresh }) { return (
+ {confirmModal}
{isRec && recorder.live_asset_id ? @@ -684,6 +833,11 @@ function RecorderRow({ recorder: initialRecorder, onRefresh }) { {recorder.status.toUpperCase()} {recorder.source} + {recorder.capturePort && ( + + {recorder.capturePort} + + )}
{recorder.url}
@@ -707,12 +861,10 @@ function RecorderRow({ recorder: initialRecorder, onRefresh }) { {displaySignal}
- {liveStatus?.currentFps != null && ( -
-
FPS
-
{Number(liveStatus.currentFps).toFixed(1)}
-
- )} +
+
Framerate
+
{displayFramerate}
+
{!isRec && ( @@ -997,6 +1149,7 @@ function Capture({ navigate }) { /* ===== Monitors ===== */ function Monitors({ navigate }) { const [recorders, setRecorders] = React.useState(window.ZAMPP_DATA?.RECORDERS || []); + const [channels, setChannels] = React.useState([]); const [grid, setGrid] = React.useState(4); React.useEffect(() => { @@ -1008,9 +1161,14 @@ function Monitors({ navigate }) { setRecorders(norm); }) .catch(() => {}); + // Playout channels surface here too so an operator can watch on-air + // output alongside ingest. Degrade silently if the endpoint is absent. + window.ZAMPP_API.fetch('/playout/channels') + .then(raw => setChannels(Array.isArray(raw) ? raw : [])) + .catch(() => setChannels([])); }; refresh(); - const id = setInterval(refresh, 5000); + const id = setInterval(refresh, 3000); return () => clearInterval(id); }, []); @@ -1032,22 +1190,134 @@ function Monitors({ navigate }) {
- {feeds.length === 0 ? ( -
No active feeds. Start a recorder to see live video here.
+ {feeds.length === 0 && channels.length === 0 ? ( +
No active feeds. Start a recorder or playout channel to see live video here.
) : ( -
- {feeds.map((f, i) => )} -
+ + {feeds.length > 0 && ( + +
Ingest
+
+ {feeds.map((f, i) => )} +
+
+ )} + {channels.length > 0 && ( + +
Playout
+
+ {channels.slice(0, grid * grid).map(c => )} +
+
+ )} +
)}
); } +function PlayoutMonitorTile({ channel }) { + const videoRef = React.useRef(null); + const hlsRef = React.useRef(null); + const onAir = channel.status === 'running'; + const previewUrl = '/api/v1/playout/channels/' + channel.id + '/hls/index.m3u8'; + + React.useEffect(() => { + const vid = videoRef.current; + if (!vid) return; + if (hlsRef.current) { try { hlsRef.current.destroy(); } catch (_) {} hlsRef.current = null; } + if (!onAir) { vid.src = ''; return; } + + if (window.Hls && window.Hls.isSupported()) { + const hls = new window.Hls({ + liveSyncDurationCount: 3, + liveMaxLatencyDurationCount: 6, + xhrSetup: (xhr) => { xhr.withCredentials = true; }, + }); + hlsRef.current = hls; + hls.loadSource(previewUrl); + hls.attachMedia(vid); + hls.on(window.Hls.Events.MANIFEST_PARSED, () => vid.play().catch(() => {})); + } else if (vid.canPlayType('application/vnd.apple.mpegurl')) { + vid.src = previewUrl; + vid.play().catch(() => {}); + } + return () => { + if (hlsRef.current) { try { hlsRef.current.destroy(); } catch (_) {} hlsRef.current = null; } + }; + }, [onAir, channel.id]); + + return ( +
+ {onAir ? ( +