Compare commits
119 commits
feat/playo
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6895fbc5af | ||
|
|
8a675992c2 | ||
| 6908801822 | |||
|
|
4d1b23959c | ||
|
|
d1d3033ed5 | ||
|
|
46dc17ffb1 | ||
|
|
c083d1006a | ||
|
|
9ec2997f53 | ||
|
|
4a3bf18f7f | ||
|
|
1c068b470e | ||
|
|
e4154ea83a | ||
|
|
cf928d1a46 | ||
|
|
f2f3a88308 | ||
|
|
b697d356b2 | ||
| ebeaf01a67 | |||
|
|
a61e385693 | ||
| bf7189218a | |||
| d66d4bea80 | |||
| cad1e52c38 | |||
|
|
6979f07307 | ||
|
|
eba8e94887 | ||
| a06b5ed304 | |||
| 7d704d3af3 | |||
| b324878db9 | |||
|
|
9809cdd13e | ||
| 75f265534e | |||
| 8b8a19c465 | |||
| 9adcae0329 | |||
| 63654ea0ed | |||
| 0eac34529b | |||
| d6e515e1a8 | |||
| 3d3c8c48de | |||
| b65ce5b0b7 | |||
| 323d482eab | |||
| 8f33cbfa86 | |||
| 12d76edc42 | |||
| 551377e4c9 | |||
| b875376887 | |||
| 252aa713d4 | |||
| 068e2eaa87 | |||
| e3be8745d3 | |||
| 7d408035ac | |||
| 884c8829a0 | |||
| 962c7c8f20 | |||
| b46abc9b1a | |||
| adfbeac217 | |||
| d4924b044f | |||
| 4da7bf8b41 | |||
| 6fec41aaf9 | |||
| de6e44b991 | |||
| c7e07df515 | |||
| 67c071a0ee | |||
| 3529590160 | |||
| 96f4f2dd3b | |||
| 1750298bb8 | |||
| 298cb18914 | |||
| e8a1f564b0 | |||
| bda33fedca | |||
|
|
66ff7065ec | ||
| c1512e29c5 | |||
| 06e480f2b4 | |||
|
|
9bcbac558c | ||
| 2cd20a0e72 | |||
| 35165e28a8 | |||
| 32a2d0329e | |||
| b92a5bc7f7 | |||
| 7b878d48c9 | |||
| 64bbb221f7 | |||
|
|
ef329399f1 | ||
| 984a73e8ec | |||
| 8a958046ef | |||
| 08499b93b2 | |||
| ca1eec0600 | |||
| 794b9d9929 | |||
| 1d642bd437 | |||
| fffff1c016 | |||
| 549ca6c73f | |||
| ea615c8c76 | |||
| f2d5f5aa16 | |||
| d908c0c056 | |||
| b597ffd58e | |||
| be8fd691a5 | |||
| dba3435e60 | |||
| 43011bd794 | |||
| 43656a5e88 | |||
| 68461af990 | |||
| 8bc460025d | |||
| 3578c7b4e9 | |||
| cddcc9a29e | |||
| 0e844c0fc3 | |||
| 551af09dc7 | |||
| 4d6a999665 | |||
| f971d57bb9 | |||
| 7ab70948a0 | |||
| 13bbd4216e | |||
| fcd8e8dd2e | |||
| 67ac007706 | |||
| b4f2fb12ff | |||
| aa7f836493 | |||
| c2409bd037 | |||
| 42064acefa | |||
| 2e2b091653 | |||
|
|
c502d4a16f | ||
|
|
9d098e9778 | ||
|
|
02631f7b96 | ||
|
|
9436434599 | ||
|
|
f837e57969 | ||
| 68c8f47c8f | |||
|
|
17bf086ef2 | ||
|
|
dac5213354 | ||
|
|
3f203f326e | ||
|
|
7e9f1277d4 | ||
|
|
9d8adbbbc1 | ||
|
|
3430ef823e | ||
|
|
08a0fb1b60 | ||
|
|
dd438b597a | ||
|
|
8746d71af1 | ||
|
|
6a161c7133 | ||
| 79369c378a |
113 changed files with 12982 additions and 968 deletions
|
|
@ -22,6 +22,11 @@ SESSION_SECRET=changeme
|
||||||
# MAM API Configuration
|
# MAM API Configuration
|
||||||
MAM_API_URL=http://mam-api:3000
|
MAM_API_URL=http://mam-api:3000
|
||||||
|
|
||||||
|
# Node Agent Authentication
|
||||||
|
# Bearer token for node-agent to authenticate with mam-api /driver/* endpoints.
|
||||||
|
# Generate with: openssl rand -hex 32
|
||||||
|
NODE_AGENT_TOKEN=changeme
|
||||||
|
|
||||||
# Auth — default to ON in production. Setting to 'false' is a dev-only escape
|
# Auth — default to ON in production. Setting to 'false' is a dev-only escape
|
||||||
# hatch that disables all auth checks and attaches a synthetic 'dev' user to
|
# hatch that disables all auth checks and attaches a synthetic 'dev' user to
|
||||||
# every request. Never run with AUTH_ENABLED=false on a network you don't control.
|
# every request. Never run with AUTH_ENABLED=false on a network you don't control.
|
||||||
|
|
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -27,8 +27,8 @@ services/editor/**/node_modules
|
||||||
services/editor/**/dist
|
services/editor/**/dist
|
||||||
services/editor/.pnpm-store
|
services/editor/.pnpm-store
|
||||||
|
|
||||||
# Blackmagic DeckLink SDK + runtime libs (operator-supplied; see services/capture/build-with-decklink.sh)
|
# Blackmagic DeckLink SDK headers are now committed (private/internal repo) under services/capture/sdk/.
|
||||||
services/capture/sdk/
|
# Runtime .so libs (libDeckLinkAPI.so) come from the DesktopVideo driver install and are not committed.
|
||||||
services/capture/lib/
|
services/capture/lib/
|
||||||
|
|
||||||
# Editor backups
|
# Editor backups
|
||||||
|
|
|
||||||
325
deploy/install-driver.sh
Normal file
325
deploy/install-driver.sh
Normal file
|
|
@ -0,0 +1,325 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# ============================================================================
|
||||||
|
# install-driver.sh <vendor>
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Idempotent HOST installer for capture-card runtime drivers / SDKs.
|
||||||
|
#
|
||||||
|
# Runs ON the cluster node's HOST kernel. The node-agent invokes it inside a
|
||||||
|
# one-shot PRIVILEGED ubuntu container that bind-mounts this repo plus the host
|
||||||
|
# paths needed to affect the host kernel (/lib/modules, /usr/src, /boot, /dev,
|
||||||
|
# and the host apt/dpkg via the mounted root). dkms / modprobe / ldconfig
|
||||||
|
# therefore operate against the running host kernel.
|
||||||
|
#
|
||||||
|
# Reads the proprietary vendor file(s) from sdk/<vendor>/ (in this repo).
|
||||||
|
# NO binaries are committed — if the expected file is missing the script exits
|
||||||
|
# non-zero with a clear message telling the operator what to drop in.
|
||||||
|
#
|
||||||
|
# Vendors (ALLOWLIST — nothing else is accepted):
|
||||||
|
# blackmagic Desktop Video .deb (DKMS kernel module)
|
||||||
|
# aja NTV2 driver source/SDK (built kernel module)
|
||||||
|
# deltacast VideoMaster installer (kernel module)
|
||||||
|
# ndi redistributable runtime libs (user-space only, no module)
|
||||||
|
#
|
||||||
|
# Exit codes:
|
||||||
|
# 0 installed (or already present / up to date)
|
||||||
|
# 2 bad usage / unknown vendor
|
||||||
|
# 3 expected vendor file missing in sdk/<vendor>/
|
||||||
|
# 4 missing kernel headers (cannot build DKMS / module)
|
||||||
|
# 5 build / install / module-load failure
|
||||||
|
#
|
||||||
|
# `bash -n` must pass. set -euo pipefail with `|| true` guarding every probe.
|
||||||
|
# ============================================================================
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Resolve our own repo dir (deploy/ -> repo root), regardless of CWD.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
|
||||||
|
REPO_DIR="$(cd "$SCRIPT_DIR/.." >/dev/null 2>&1 && pwd)"
|
||||||
|
SDK_ROOT="$REPO_DIR/sdk"
|
||||||
|
|
||||||
|
VENDOR="${1:-}"
|
||||||
|
KVER="$(uname -r 2>/dev/null || echo unknown)"
|
||||||
|
REBOOT_REQUIRED=0
|
||||||
|
|
||||||
|
log() { echo "[install-driver] $*"; }
|
||||||
|
warn() { echo "[install-driver] WARN: $*" >&2; }
|
||||||
|
die() { echo "[install-driver] ERROR: $*" >&2; exit "${2:-5}"; }
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
echo "Usage: install-driver.sh <blackmagic|aja|deltacast|ndi>" >&2
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
|
||||||
|
[ -n "$VENDOR" ] || usage
|
||||||
|
case "$VENDOR" in
|
||||||
|
blackmagic|aja|deltacast|ndi) : ;;
|
||||||
|
*) echo "[install-driver] ERROR: unknown vendor '$VENDOR' (allowed: blackmagic aja deltacast ndi)" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
VENDOR_DIR="$SDK_ROOT/$VENDOR"
|
||||||
|
log "vendor=$VENDOR kernel=$KVER repo=$REPO_DIR"
|
||||||
|
log "reading vendor files from $VENDOR_DIR"
|
||||||
|
[ -d "$VENDOR_DIR" ] || die "vendor dir $VENDOR_DIR does not exist (repo not mounted?)" 3
|
||||||
|
|
||||||
|
# Pick the newest file matching a glob; echo its path or empty.
|
||||||
|
newest_match() {
|
||||||
|
# shellcheck disable=SC2012
|
||||||
|
ls -1t $1 2>/dev/null | head -n1 || true
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_headers() {
|
||||||
|
if [ -d "/lib/modules/$KVER/build" ] || dpkg -s "linux-headers-$KVER" >/dev/null 2>&1; then
|
||||||
|
log "kernel headers for $KVER present"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
log "installing linux-headers-$KVER ..."
|
||||||
|
apt-get update -y >/dev/null 2>&1 || true
|
||||||
|
if ! apt-get install -y "linux-headers-$KVER" >/dev/null 2>&1; then
|
||||||
|
# Fall back to the generic meta-package; still may not match a custom kernel.
|
||||||
|
apt-get install -y linux-headers-generic >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
if [ -d "/lib/modules/$KVER/build" ] || dpkg -s "linux-headers-$KVER" >/dev/null 2>&1; then
|
||||||
|
log "kernel headers ready"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
die "kernel headers for $KVER unavailable — cannot build module. Install linux-headers-$KVER on the host." 4
|
||||||
|
}
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# blackmagic — Desktop Video .deb (DKMS) + modprobe + restart desktopvideo
|
||||||
|
# ===========================================================================
|
||||||
|
install_blackmagic() {
|
||||||
|
# Detect existing state first (idempotent skip).
|
||||||
|
if lsmod 2>/dev/null | grep -q '^blackmagic' && [ -e /dev/blackmagic ]; then
|
||||||
|
log "blackmagic module loaded and /dev/blackmagic present — already installed"
|
||||||
|
if command -v dkms >/dev/null 2>&1; then dkms status 2>/dev/null | grep -i blackmagic || true; fi
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local deb
|
||||||
|
deb="$(newest_match "$VENDOR_DIR/desktopvideo_*_amd64.deb")"
|
||||||
|
[ -n "$deb" ] && [ -f "$deb" ] || die \
|
||||||
|
"no desktopvideo_*_amd64.deb in $VENDOR_DIR — download Desktop Video for Ubuntu 22.04 and drop the .deb there (see sdk/blackmagic/README.md)." 3
|
||||||
|
log "using package: $(basename "$deb")"
|
||||||
|
|
||||||
|
ensure_headers
|
||||||
|
|
||||||
|
log "apt-get install (DKMS build) ..."
|
||||||
|
apt-get update -y >/dev/null 2>&1 || true
|
||||||
|
apt-get install -y "$deb" || die "apt-get install of $(basename "$deb") failed (DKMS build?)" 5
|
||||||
|
|
||||||
|
log "depmod + modprobe blackmagic ..."
|
||||||
|
depmod -a "$KVER" 2>/dev/null || true
|
||||||
|
if ! modprobe blackmagic 2>/dev/null; then
|
||||||
|
warn "modprobe blackmagic failed — a reboot may be required for the DKMS module to bind"
|
||||||
|
REBOOT_REQUIRED=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Restart the DesktopVideoHelper daemon if present.
|
||||||
|
if command -v systemctl >/dev/null 2>&1; then
|
||||||
|
systemctl restart desktopvideo 2>/dev/null \
|
||||||
|
|| systemctl restart DesktopVideoHelper 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if lsmod 2>/dev/null | grep -q '^blackmagic' || [ -e /dev/blackmagic ]; then
|
||||||
|
log "blackmagic installed (module loaded / device present)"
|
||||||
|
else
|
||||||
|
warn "blackmagic installed but module not yet loaded — reboot required"
|
||||||
|
REBOOT_REQUIRED=1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# aja — build ntv2 kernel module from source archive + modprobe
|
||||||
|
# ===========================================================================
|
||||||
|
install_aja() {
|
||||||
|
if lsmod 2>/dev/null | grep -q 'ajantv2'; then
|
||||||
|
log "ajantv2 module already loaded — already installed"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local src
|
||||||
|
src="$(newest_match "$VENDOR_DIR/ntv2sdk*.zip")"
|
||||||
|
[ -n "$src" ] || src="$(newest_match "$VENDOR_DIR/libajantv2*.tar.gz")"
|
||||||
|
[ -n "$src" ] && [ -f "$src" ] || die \
|
||||||
|
"no ntv2sdk*.zip / libajantv2*.tar.gz in $VENDOR_DIR — download the AJA NTV2 Linux SDK and drop it there (see sdk/aja/README.md)." 3
|
||||||
|
log "using source: $(basename "$src")"
|
||||||
|
|
||||||
|
ensure_headers
|
||||||
|
apt-get install -y build-essential unzip >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
local work; work="$(mktemp -d)"
|
||||||
|
log "extracting into $work ..."
|
||||||
|
case "$src" in
|
||||||
|
*.zip) unzip -q -o "$src" -d "$work" || die "failed to unzip $(basename "$src")" 5 ;;
|
||||||
|
*.tar.gz) tar -xzf "$src" -C "$work" || die "failed to untar $(basename "$src")" 5 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Find the linux driver dir (contains a Makefile producing ajantv2.ko).
|
||||||
|
local drvdir
|
||||||
|
drvdir="$(dirname "$(find "$work" -type d -path '*driver/linux' -print -quit 2>/dev/null || true)/." 2>/dev/null)"
|
||||||
|
[ -d "$drvdir" ] || drvdir="$(dirname "$(find "$work" -name 'ajantv2.c' -print -quit 2>/dev/null || true)" 2>/dev/null)"
|
||||||
|
[ -n "$drvdir" ] && [ -d "$drvdir" ] || die "could not locate AJA driver/linux source inside the archive" 5
|
||||||
|
log "building module in $drvdir ..."
|
||||||
|
make -C "$drvdir" >/dev/null 2>&1 || die "AJA module build failed" 5
|
||||||
|
|
||||||
|
local ko
|
||||||
|
ko="$(find "$drvdir" -name 'ajantv2.ko' -print -quit 2>/dev/null || true)"
|
||||||
|
if [ -n "$ko" ]; then
|
||||||
|
install -D -m 0644 "$ko" "/lib/modules/$KVER/extra/ajantv2.ko" 2>/dev/null || true
|
||||||
|
depmod -a "$KVER" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
if ! modprobe ajantv2 2>/dev/null; then
|
||||||
|
# Fall back to the SDK's own load script if shipped.
|
||||||
|
local loader; loader="$(find "$work" -name 'load_ajantv2' -print -quit 2>/dev/null || true)"
|
||||||
|
if [ -n "$loader" ]; then bash "$loader" 2>/dev/null || true; fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -rf "$work" 2>/dev/null || true
|
||||||
|
|
||||||
|
if lsmod 2>/dev/null | grep -q 'ajantv2'; then
|
||||||
|
log "ajantv2 installed and loaded"
|
||||||
|
else
|
||||||
|
warn "ajantv2 built but not loaded — a reboot may be required (old in-tree module wedged?)"
|
||||||
|
REBOOT_REQUIRED=1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# deltacast — VideoMaster installer + module load
|
||||||
|
# ===========================================================================
|
||||||
|
install_deltacast() {
|
||||||
|
if lsmod 2>/dev/null | grep -q 'videomaster' || ls /dev/deltacast* >/dev/null 2>&1; then
|
||||||
|
log "Deltacast module loaded / device present — already installed"
|
||||||
|
install_deltacast_udev_rule
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local pkg
|
||||||
|
pkg="$(newest_match "$VENDOR_DIR/VideoMaster*.run")"
|
||||||
|
[ -n "$pkg" ] || pkg="$(newest_match "$VENDOR_DIR/VideoMaster*.tar.gz")"
|
||||||
|
[ -n "$pkg" ] && [ -f "$pkg" ] || die \
|
||||||
|
"no VideoMaster*.run / VideoMaster*.tar.gz in $VENDOR_DIR — obtain the Deltacast VideoMaster Linux installer and drop it there (see sdk/deltacast/README.md)." 3
|
||||||
|
log "using installer: $(basename "$pkg")"
|
||||||
|
|
||||||
|
ensure_headers
|
||||||
|
apt-get install -y build-essential dkms >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
local work; work="$(mktemp -d)"
|
||||||
|
case "$pkg" in
|
||||||
|
*.run)
|
||||||
|
log "extracting self-extractor ..."
|
||||||
|
chmod +x "$pkg" 2>/dev/null || true
|
||||||
|
"$pkg" --noexec --target "$work" >/dev/null 2>&1 \
|
||||||
|
|| cp "$pkg" "$work/installer.run"
|
||||||
|
;;
|
||||||
|
*.tar.gz)
|
||||||
|
tar -xzf "$pkg" -C "$work" || die "failed to untar $(basename "$pkg")" 5
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
local installer
|
||||||
|
installer="$(find "$work" -name 'install.sh' -print -quit 2>/dev/null || true)"
|
||||||
|
[ -n "$installer" ] || installer="$(find "$work" -name 'installer.run' -print -quit 2>/dev/null || true)"
|
||||||
|
[ -n "$installer" ] && [ -f "$installer" ] || die "Deltacast installer (install.sh) not found inside the package" 5
|
||||||
|
log "running vendor installer: $(basename "$installer") ..."
|
||||||
|
chmod +x "$installer" 2>/dev/null || true
|
||||||
|
( cd "$(dirname "$installer")" && bash "$installer" ) || die "Deltacast VideoMaster installer failed" 5
|
||||||
|
|
||||||
|
depmod -a "$KVER" 2>/dev/null || true
|
||||||
|
modprobe videomasterhd 2>/dev/null || modprobe videomaster 2>/dev/null || true
|
||||||
|
|
||||||
|
rm -rf "$work" 2>/dev/null || true
|
||||||
|
|
||||||
|
if lsmod 2>/dev/null | grep -q 'videomaster' || ls /dev/deltacast* >/dev/null 2>&1; then
|
||||||
|
log "Deltacast VideoMaster installed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install our own udev rule that creates 8 /dev/deltacast symlinks (ports 0-7)
|
||||||
|
# pointing at the single real device node. Kept separate from the SDK's own
|
||||||
|
# rule so a driver reinstall won't clobber it.
|
||||||
|
install_deltacast_udev_rule
|
||||||
|
|
||||||
|
# First-time VideoMaster installs lay down udev rules + firmware that need a reboot.
|
||||||
|
warn "Deltacast: a REBOOT is recommended after a first-time VideoMaster install (udev + firmware)"
|
||||||
|
REBOOT_REQUIRED=1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Copy the repo's 99-wild-dragon-deltacast.rules into /etc/udev/rules.d/ and
|
||||||
|
# reload. Idempotent. Creates /dev/deltacast0..7 -> /dev/delta-x3700 so the
|
||||||
|
# node-agent advertises all 8 RX channels.
|
||||||
|
install_deltacast_udev_rule() {
|
||||||
|
local rule_src="$REPO_DIR/deploy/udev/99-wild-dragon-deltacast.rules"
|
||||||
|
local rule_dst="/etc/udev/rules.d/99-wild-dragon-deltacast.rules"
|
||||||
|
if [ ! -f "$rule_src" ]; then
|
||||||
|
warn "Deltacast: udev rule $rule_src not found in repo — skipping symlink rule install"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [ -f "$rule_dst" ] && cmp -s "$rule_src" "$rule_dst"; then
|
||||||
|
log "Deltacast: udev rule already up to date at $rule_dst"
|
||||||
|
else
|
||||||
|
log "installing Deltacast udev rule -> $rule_dst"
|
||||||
|
install -D -m 0644 "$rule_src" "$rule_dst" 2>/dev/null \
|
||||||
|
|| { warn "Deltacast: failed to install udev rule (continuing)"; return 0; }
|
||||||
|
udevadm control --reload-rules 2>/dev/null || true
|
||||||
|
udevadm trigger --action=add /dev/delta-x3700 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# ndi — copy redistributable runtime libs to /usr/local/lib + ldconfig
|
||||||
|
# ===========================================================================
|
||||||
|
install_ndi() {
|
||||||
|
local target="/opt/ndi-lib"
|
||||||
|
local found=0
|
||||||
|
# shellcheck disable=SC2231
|
||||||
|
for f in "$VENDOR_DIR"/libndi*.so*; do
|
||||||
|
[ -e "$f" ] || continue
|
||||||
|
found=1
|
||||||
|
break
|
||||||
|
done
|
||||||
|
[ "$found" = 1 ] || die \
|
||||||
|
"no libndi*.so* in $VENDOR_DIR — drop the NDI runtime redistributable libs there (see sdk/ndi/README.md)." 3
|
||||||
|
|
||||||
|
log "copying NDI runtime libs to $target ..."
|
||||||
|
mkdir -p "$target"
|
||||||
|
cp -av "$VENDOR_DIR"/libndi*.so* "$target"/ 2>/dev/null || die "failed copying NDI libs" 5
|
||||||
|
|
||||||
|
# Recreate the libndi.so dev symlink if only versioned libs were shipped.
|
||||||
|
if [ ! -e "$target/libndi.so" ]; then
|
||||||
|
local versioned
|
||||||
|
versioned="$(newest_match "$target/libndi.so.*")"
|
||||||
|
if [ -n "$versioned" ]; then
|
||||||
|
ln -sf "$(basename "$versioned")" "$target/libndi.so" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$target" > /etc/ld.so.conf.d/ndi.conf
|
||||||
|
ldconfig 2>/dev/null || true
|
||||||
|
|
||||||
|
if ldconfig -p 2>/dev/null | grep -q 'libndi'; then
|
||||||
|
log "NDI runtime registered with the dynamic linker"
|
||||||
|
else
|
||||||
|
die "NDI libs copied but ldconfig did not resolve libndi" 5
|
||||||
|
fi
|
||||||
|
log "NDI: no kernel module and no reboot required."
|
||||||
|
log "NDI: restart any process that already loaded an older libndi to pick up the new version."
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
case "$VENDOR" in
|
||||||
|
blackmagic) install_blackmagic ;;
|
||||||
|
aja) install_aja ;;
|
||||||
|
deltacast) install_deltacast ;;
|
||||||
|
ndi) install_ndi ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ "$REBOOT_REQUIRED" = 1 ]; then
|
||||||
|
log "RESULT: $VENDOR install completed — REBOOT REQUIRED"
|
||||||
|
echo "[install-driver] REBOOT_REQUIRED=1"
|
||||||
|
else
|
||||||
|
log "RESULT: $VENDOR install completed — no reboot required"
|
||||||
|
echo "[install-driver] REBOOT_REQUIRED=0"
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
|
@ -16,11 +16,12 @@
|
||||||
# Environment variables:
|
# Environment variables:
|
||||||
# MAM_API_URL REQUIRED Primary MAM API base URL
|
# MAM_API_URL REQUIRED Primary MAM API base URL
|
||||||
# NODE_TOKEN API bearer token (required if AUTH_ENABLED=true)
|
# NODE_TOKEN API bearer token (required if AUTH_ENABLED=true)
|
||||||
# NODE_ROLE Role tag reported to the cluster (default: worker)
|
# NODE_ROLE Role tag reported to the cluster (default: auto-detect)
|
||||||
# NODE_IP Override the LAN IP reported back (default: auto-detect)
|
# NODE_IP Override the LAN IP reported back (default: auto-detect)
|
||||||
# AGENT_PORT Host port for the node agent (default: 7436)
|
# AGENT_PORT Host port for the node agent (default: 7436)
|
||||||
# INSTALL_DIR Where to clone/find the repo (default: /opt/wild-dragon)
|
# INSTALL_DIR Where to clone/find the repo (default: /opt/wild-dragon)
|
||||||
# PROFILES Extra compose profiles, space-sep e.g. "worker capture"
|
# PROFILES Compose profiles, space-sep (default: auto-detect from hardware)
|
||||||
|
# Override only to force, e.g. "worker capture"
|
||||||
# BMD_MODEL DeckLink card model name (e.g. "DeckLink Duo 2")
|
# BMD_MODEL DeckLink card model name (e.g. "DeckLink Duo 2")
|
||||||
# REPO_URL Override the Forgejo clone URL
|
# REPO_URL Override the Forgejo clone URL
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
@ -32,8 +33,16 @@ REPO_URL="${REPO_URL:-https://forge.wilddragon.net/zgaetano/wild-dragon.git}"
|
||||||
INSTALL_DIR="${INSTALL_DIR:-/opt/wild-dragon}"
|
INSTALL_DIR="${INSTALL_DIR:-/opt/wild-dragon}"
|
||||||
MAM_API_URL="${MAM_API_URL:-}"
|
MAM_API_URL="${MAM_API_URL:-}"
|
||||||
NODE_TOKEN="${NODE_TOKEN:-}"
|
NODE_TOKEN="${NODE_TOKEN:-}"
|
||||||
|
# Track whether the caller pinned NODE_ROLE explicitly (manual override) vs.
|
||||||
|
# us defaulting it — so auto-detection only fills in an *unset* role.
|
||||||
|
[[ -n "${NODE_ROLE:-}" ]] && NODE_ROLE_EXPLICIT=1 || NODE_ROLE_EXPLICIT=""
|
||||||
NODE_ROLE="${NODE_ROLE:-worker}"
|
NODE_ROLE="${NODE_ROLE:-worker}"
|
||||||
NODE_IP="${NODE_IP:-}"
|
NODE_IP="${NODE_IP:-}"
|
||||||
|
# NODE_NAME pins this node's cluster identity (the heartbeat key). Default to the
|
||||||
|
# OS hostname, but ALWAYS write it explicitly so cloned VMs that share an
|
||||||
|
# /etc/hostname (e.g. two boxes both named "zampp1") don't collide on the same
|
||||||
|
# cluster_nodes row — which silently hides the capture node's DeckLink devices.
|
||||||
|
NODE_NAME="${NODE_NAME:-$(hostname)}"
|
||||||
AGENT_PORT="${AGENT_PORT:-7436}"
|
AGENT_PORT="${AGENT_PORT:-7436}"
|
||||||
PROFILES="${PROFILES:-}"
|
PROFILES="${PROFILES:-}"
|
||||||
BMD_MODEL="${BMD_MODEL:-}"
|
BMD_MODEL="${BMD_MODEL:-}"
|
||||||
|
|
@ -65,6 +74,37 @@ detect_lan_ip() {
|
||||||
echo "$ip"
|
echo "$ip"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── Auto-detect hardware ─────────────────────────────────────────────────────
|
||||||
|
# Mirror detect_lan_ip's style: best-effort, guard every probe with `|| true`
|
||||||
|
# so a missing nvidia-smi/lspci never aborts under `set -euo pipefail`. The
|
||||||
|
# node self-describes its hardware here so the operator never has to pick a
|
||||||
|
# role — the right compose profiles are enabled automatically.
|
||||||
|
|
||||||
|
# GPU present? nvidia-smi is the strong signal; fall back to an lspci scan for
|
||||||
|
# NVIDIA or AMD VGA controllers (covers nodes where the driver isn't installed
|
||||||
|
# yet but the card is physically present).
|
||||||
|
detect_gpu() {
|
||||||
|
if command -v nvidia-smi &>/dev/null && nvidia-smi -L &>/dev/null; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if command -v lspci &>/dev/null; then
|
||||||
|
if lspci 2>/dev/null | grep -iE 'nvidia|vga.*amd' &>/dev/null; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# SDI capture card present? Blackmagic DeckLink or Deltacast, via lspci.
|
||||||
|
detect_sdi() {
|
||||||
|
if command -v lspci &>/dev/null; then
|
||||||
|
if lspci 2>/dev/null | grep -iE 'blackmagic|deltacast' &>/dev/null; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
# ── Preflight ────────────────────────────────────────────────────────────────
|
# ── Preflight ────────────────────────────────────────────────────────────────
|
||||||
echo -e "\n${BLD}${CYN}Wild Dragon MAM — Cluster Node Onboarding${NC}\n"
|
echo -e "\n${BLD}${CYN}Wild Dragon MAM — Cluster Node Onboarding${NC}\n"
|
||||||
|
|
||||||
|
|
@ -79,6 +119,36 @@ if [[ -z "$NODE_IP" ]]; then
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ── Auto-assign compose profiles from detected hardware ──────────────────────
|
||||||
|
# Operator never picks a role: the worker profile always runs, and we add the
|
||||||
|
# gpu / capture profiles only when the matching hardware is present. Explicit
|
||||||
|
# PROFILES / NODE_ROLE env vars are honoured as a manual override escape hatch.
|
||||||
|
HAS_GPU=false; HAS_SDI=false
|
||||||
|
detect_gpu && HAS_GPU=true || true
|
||||||
|
detect_sdi && HAS_SDI=true || true
|
||||||
|
|
||||||
|
DETECTED_DESC="CPU"
|
||||||
|
[[ "$HAS_GPU" == true ]] && DETECTED_DESC="$DETECTED_DESC, GPU"
|
||||||
|
[[ "$HAS_SDI" == true ]] && DETECTED_DESC="$DETECTED_DESC, SDI capture card"
|
||||||
|
|
||||||
|
if [[ -z "$PROFILES" ]]; then
|
||||||
|
AUTO_PROFILES="worker"
|
||||||
|
[[ "$HAS_GPU" == true ]] && AUTO_PROFILES="$AUTO_PROFILES gpu"
|
||||||
|
[[ "$HAS_SDI" == true ]] && AUTO_PROFILES="$AUTO_PROFILES capture"
|
||||||
|
PROFILES="$AUTO_PROFILES"
|
||||||
|
info "Detected: $DETECTED_DESC → profiles: $PROFILES"
|
||||||
|
else
|
||||||
|
info "Detected: $DETECTED_DESC (profiles overridden by env: $PROFILES)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Derive a human-friendly role tag from detected hardware when not pinned.
|
||||||
|
# Capture cards win over GPU (an SDI ingest node is the more specific role).
|
||||||
|
if [[ -z "$NODE_ROLE_EXPLICIT" ]]; then
|
||||||
|
if [[ "$HAS_SDI" == true ]]; then NODE_ROLE="capture"
|
||||||
|
elif [[ "$HAS_GPU" == true ]]; then NODE_ROLE="gpu"
|
||||||
|
else NODE_ROLE="worker"; fi
|
||||||
|
fi
|
||||||
|
|
||||||
info "Primary API : $MAM_API_URL"
|
info "Primary API : $MAM_API_URL"
|
||||||
info "Role : $NODE_ROLE"
|
info "Role : $NODE_ROLE"
|
||||||
info "Agent port : $AGENT_PORT"
|
info "Agent port : $AGENT_PORT"
|
||||||
|
|
@ -135,6 +205,7 @@ info "Writing $ENV_FILE"
|
||||||
echo "MAM_API_URL=$MAM_API_URL"
|
echo "MAM_API_URL=$MAM_API_URL"
|
||||||
echo "NODE_TOKEN=$NODE_TOKEN"
|
echo "NODE_TOKEN=$NODE_TOKEN"
|
||||||
echo "NODE_ROLE=$NODE_ROLE"
|
echo "NODE_ROLE=$NODE_ROLE"
|
||||||
|
echo "NODE_NAME=$NODE_NAME"
|
||||||
echo "NODE_IP=$NODE_IP"
|
echo "NODE_IP=$NODE_IP"
|
||||||
echo "AGENT_PORT=$AGENT_PORT"
|
echo "AGENT_PORT=$AGENT_PORT"
|
||||||
echo "HEARTBEAT_MS=30000"
|
echo "HEARTBEAT_MS=30000"
|
||||||
|
|
|
||||||
1
deploy/udev/99-wild-dragon-deltacast.rules
Normal file
1
deploy/udev/99-wild-dragon-deltacast.rules
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
KERNEL=="delta-x3700", MODE="0666", RUN+="/bin/sh -c 'for i in 0 1 2 3 4 5 6 7; do ln -sf /dev/delta-x3700 /dev/deltacast$i; done'"
|
||||||
|
|
@ -47,6 +47,10 @@ services:
|
||||||
MAM_API_URL: ${MAM_API_URL}
|
MAM_API_URL: ${MAM_API_URL}
|
||||||
NODE_TOKEN: ${NODE_TOKEN:-}
|
NODE_TOKEN: ${NODE_TOKEN:-}
|
||||||
NODE_ROLE: ${NODE_ROLE:-worker}
|
NODE_ROLE: ${NODE_ROLE:-worker}
|
||||||
|
# NODE_NAME pins the cluster identity (heartbeat key). Set it per-node so
|
||||||
|
# cloned VMs that share /etc/hostname don't collide on the same
|
||||||
|
# cluster_nodes row. Falls back to the OS hostname when unset.
|
||||||
|
NODE_NAME: ${NODE_NAME:-}
|
||||||
NODE_IP: ${NODE_IP:-}
|
NODE_IP: ${NODE_IP:-}
|
||||||
AGENT_PORT: ${AGENT_PORT:-7436}
|
AGENT_PORT: ${AGENT_PORT:-7436}
|
||||||
HEARTBEAT_MS: ${HEARTBEAT_MS:-30000}
|
HEARTBEAT_MS: ${HEARTBEAT_MS:-30000}
|
||||||
|
|
@ -55,10 +59,25 @@ services:
|
||||||
BMD_MODEL: ${BMD_MODEL:-}
|
BMD_MODEL: ${BMD_MODEL:-}
|
||||||
BMD_DEVICE_PREFIX: ${BMD_DEVICE_PREFIX:-dv}
|
BMD_DEVICE_PREFIX: ${BMD_DEVICE_PREFIX:-dv}
|
||||||
LIVE_DIR: ${LIVE_DIR:-/mnt/NVME/MAM/wild-dragon-live}
|
LIVE_DIR: ${LIVE_DIR:-/mnt/NVME/MAM/wild-dragon-live}
|
||||||
|
# REPO_DIR: host path to the checked-out repo. The agent passes this to the
|
||||||
|
# one-shot driver-install container so install-driver.sh can read
|
||||||
|
# sdk/<vendor>/ and run deploy/install-driver.sh. Must match the host path
|
||||||
|
# bind-mounted below (onboard-node.sh clones to /opt/wild-dragon).
|
||||||
|
REPO_DIR: ${REPO_DIR:-/opt/wild-dragon}
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
- /dev:/dev:ro
|
- /dev:/dev:ro
|
||||||
- /mnt/NVME/MAM/wild-dragon-live:/mnt/NVME/MAM/wild-dragon-live:ro
|
- /mnt/NVME/MAM/wild-dragon-live:/mnt/NVME/MAM/wild-dragon-live:ro
|
||||||
|
# Capture-driver deployment ("Capture Drivers / SDKs" in the Cluster admin
|
||||||
|
# screen): the agent itself does NOT run dkms/modprobe — it spawns a
|
||||||
|
# separate privileged ubuntu container that bind-mounts these host paths.
|
||||||
|
# The agent only needs to *see* the repo path so it can pass it through as
|
||||||
|
# a bind to that container; no extra privileges are granted to the agent.
|
||||||
|
# /opt/wild-dragon → repo (sdk/<vendor>/ + deploy/install-driver.sh)
|
||||||
|
# The install container additionally mounts /lib/modules,/usr/src,/boot,
|
||||||
|
# /dev and /opt from the host (handled in the agent, not here) so DKMS /
|
||||||
|
# modprobe / ldconfig affect the host kernel.
|
||||||
|
- ${REPO_DIR:-/opt/wild-dragon}:${REPO_DIR:-/opt/wild-dragon}:ro
|
||||||
devices:
|
devices:
|
||||||
- /dev/blackmagic:/dev/blackmagic
|
- /dev/blackmagic:/dev/blackmagic
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,11 @@ services:
|
||||||
DOCKER_NETWORK: wild-dragon_wild-dragon
|
DOCKER_NETWORK: wild-dragon_wild-dragon
|
||||||
NODE_IP: ${NODE_IP}
|
NODE_IP: ${NODE_IP}
|
||||||
NODE_HOSTNAME: ${NODE_HOSTNAME:-}
|
NODE_HOSTNAME: ${NODE_HOSTNAME:-}
|
||||||
|
# Bearer mam-api forwards to a node-agent when installing capture drivers
|
||||||
|
# ("Capture Drivers / SDKs" panel). Set to the same value as the agents'
|
||||||
|
# NODE_TOKEN. If empty, agents with an empty NODE_TOKEN accept the call
|
||||||
|
# (dev); agents with a token will reject it (401).
|
||||||
|
NODE_AGENT_TOKEN: ${NODE_AGENT_TOKEN:-}
|
||||||
CAPTURE_TOKEN: ${CAPTURE_TOKEN}
|
CAPTURE_TOKEN: ${CAPTURE_TOKEN}
|
||||||
PLAYOUT_IMAGE: ${PLAYOUT_IMAGE:-wild-dragon-playout:latest}
|
PLAYOUT_IMAGE: ${PLAYOUT_IMAGE:-wild-dragon-playout:latest}
|
||||||
PLAYOUT_AMCP_BASE_PORT: ${PLAYOUT_AMCP_BASE_PORT:-5250}
|
PLAYOUT_AMCP_BASE_PORT: ${PLAYOUT_AMCP_BASE_PORT:-5250}
|
||||||
|
|
@ -107,6 +112,11 @@ services:
|
||||||
dockerfile: Dockerfile.gpu
|
dockerfile: Dockerfile.gpu
|
||||||
image: wild-dragon-worker-gpu:latest
|
image: wild-dragon-worker-gpu:latest
|
||||||
runtime: nvidia
|
runtime: nvidia
|
||||||
|
# Privileged so the promotion scanner can mount the growing-files CIFS share
|
||||||
|
# at /growing (same Approach A as the capture sidecar). Without the share
|
||||||
|
# mounted the scanner watches an empty local dir and never promotes growing
|
||||||
|
# captures to S3.
|
||||||
|
privileged: true
|
||||||
depends_on:
|
depends_on:
|
||||||
- queue
|
- queue
|
||||||
- db
|
- db
|
||||||
|
|
@ -131,7 +141,9 @@ services:
|
||||||
WORKER_LABEL: "zampp1 / Tesla P4"
|
WORKER_LABEL: "zampp1 / Tesla P4"
|
||||||
NVIDIA_DRIVER_CAPABILITIES: video,compute,utility
|
NVIDIA_DRIVER_CAPABILITIES: video,compute,utility
|
||||||
volumes:
|
volumes:
|
||||||
- /mnt/NVME/MAM/wild-dragon-growing:/growing
|
# NOTE: /growing is NOT a host bind anymore — the promotion scanner mounts
|
||||||
|
# the CIFS landing-zone share there itself (a bind would shadow it). The
|
||||||
|
# mount needs rshared propagation so the in-container CIFS mount is visible.
|
||||||
- /mnt/NVME/MAM/wild-dragon-media:/media
|
- /mnt/NVME/MAM/wild-dragon-media:/media
|
||||||
networks:
|
networks:
|
||||||
- wild-dragon
|
- wild-dragon
|
||||||
|
|
|
||||||
834
docs/superpowers/plans/2026-06-01-deltacast-sdi-capture-plan.md
Normal file
834
docs/superpowers/plans/2026-06-01-deltacast-sdi-capture-plan.md
Normal file
|
|
@ -0,0 +1,834 @@
|
||||||
|
# Deltacast SDI Capture — Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Wire up Deltacast VideoMaster SDI cards in the capture service using a C bridge binary that streams raw video to FFmpeg via pipe, with embedded audio via a named FIFO.
|
||||||
|
|
||||||
|
**Architecture:** A `deltacast-capture` C binary opens the VideoMaster board, waits for signal lock, emits a JSON format line to stderr, then streams raw UYVY video frames to stdout and 2-channel PCM audio to a named FIFO. `capture-manager.js` reads the JSON, spawns FFmpeg with `-f rawvideo -i pipe:0` for video and `-f s16le -i <fifo>` for audio, and pipes bridge stdout into FFmpeg stdin. Two concurrent SDK streams share the same board handle — `VHD_SDI_STPROC_DISJOINED_VIDEO` for video and `VHD_SDI_STPROC_DISJOINED_ANC` for audio.
|
||||||
|
|
||||||
|
**Tech Stack:** Deltacast VideoMaster C SDK 6.34.1 (`libvideomasterhd.so`, `libvideomasterhd_audio.so`), C17, CMake, Node.js ES modules, Docker multi-stage build.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Map
|
||||||
|
|
||||||
|
| Action | Path | Responsibility |
|
||||||
|
|---|---|---|
|
||||||
|
| Create | `services/capture/deltacast-bridge/CMakeLists.txt` | Build config for the bridge binary |
|
||||||
|
| Create | `services/capture/deltacast-bridge/main.c` | Bridge: board open, signal detect, video stream, audio thread |
|
||||||
|
| Modify | `services/capture/Dockerfile` | SDK extraction stage, bridge build stage, runtime .so install |
|
||||||
|
| Modify | `services/capture/src/capture-manager.js` | `readFirstStderrLine` helper, deltacast `_buildInputArgs`, bridge lifecycle in `start()`/`stop()` |
|
||||||
|
| Modify | `services/capture/src/routes/capture.js` | Accept `deltacast` as a valid `source_type` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1: Bridge CMakeLists.txt
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `services/capture/deltacast-bridge/CMakeLists.txt`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create the CMakeLists.txt**
|
||||||
|
|
||||||
|
```cmake
|
||||||
|
cmake_minimum_required(VERSION 3.16)
|
||||||
|
project(deltacast-bridge C)
|
||||||
|
set(CMAKE_C_STANDARD 17)
|
||||||
|
|
||||||
|
set(SDK_ROOT "/sdk" CACHE PATH "Path to extracted VideoMaster SDK")
|
||||||
|
|
||||||
|
add_executable(deltacast-capture main.c)
|
||||||
|
|
||||||
|
target_include_directories(deltacast-capture PRIVATE
|
||||||
|
${SDK_ROOT}/include/videomaster
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_directories(deltacast-capture PRIVATE
|
||||||
|
${SDK_ROOT}/lib
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(deltacast-capture PRIVATE
|
||||||
|
videomasterhd
|
||||||
|
videomasterhd_audio
|
||||||
|
pthread
|
||||||
|
)
|
||||||
|
|
||||||
|
# Embed the SDK RPATH so the binary finds the .so at runtime
|
||||||
|
set_target_properties(deltacast-capture PROPERTIES
|
||||||
|
INSTALL_RPATH "/usr/local/lib/deltacast"
|
||||||
|
BUILD_WITH_INSTALL_RPATH TRUE
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add services/capture/deltacast-bridge/CMakeLists.txt
|
||||||
|
git commit -m "build(capture): add CMakeLists for deltacast-capture bridge binary"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2: Bridge main.c
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `services/capture/deltacast-bridge/main.c`
|
||||||
|
|
||||||
|
The binary: parses CLI args, opens the board, waits for signal lock, emits one JSON line to stderr, then spawns an audio thread writing to a FIFO and runs a video capture loop writing raw UYVY frames to stdout.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create the bridge source file**
|
||||||
|
|
||||||
|
```c
|
||||||
|
/* services/capture/deltacast-bridge/main.c
|
||||||
|
*
|
||||||
|
* Deltacast VideoMaster SDI capture bridge.
|
||||||
|
* Writes raw UYVY video to stdout and stereo PCM to a named FIFO.
|
||||||
|
* Emits one JSON line to stderr on signal lock before streaming starts.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* deltacast-capture --device <N> --port <N> --audio-pipe <path>
|
||||||
|
* [--signal-timeout <sec>]
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <stdatomic.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include "VideoMasterHD_Core.h"
|
||||||
|
#include "VideoMasterHD_Sdi.h"
|
||||||
|
#include "VideoMasterHD_Sdi_Audio.h"
|
||||||
|
|
||||||
|
/* ── Globals ─────────────────────────────────────────────────────────── */
|
||||||
|
static atomic_int g_stop = 0;
|
||||||
|
|
||||||
|
static void on_signal(int s) { (void)s; atomic_store(&g_stop, 1); }
|
||||||
|
|
||||||
|
/* ── Stream type by port index ───────────────────────────────────────── */
|
||||||
|
static ULONG rx_streamtype(unsigned port) {
|
||||||
|
switch (port) {
|
||||||
|
case 0: return VHD_ST_RX0;
|
||||||
|
case 1: return VHD_ST_RX1;
|
||||||
|
case 2: return VHD_ST_RX2;
|
||||||
|
case 3: return VHD_ST_RX3;
|
||||||
|
default: return VHD_ST_RX0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Loopback board property by port index ───────────────────────────── */
|
||||||
|
static ULONG loopback_prop(unsigned port) {
|
||||||
|
switch (port) {
|
||||||
|
case 0: return VHD_CORE_BP_PASSIVE_LOOPBACK_0;
|
||||||
|
case 1: return VHD_CORE_BP_PASSIVE_LOOPBACK_1;
|
||||||
|
case 2: return VHD_CORE_BP_PASSIVE_LOOPBACK_2;
|
||||||
|
case 3: return VHD_CORE_BP_PASSIVE_LOOPBACK_3;
|
||||||
|
default: return VHD_CORE_BP_PASSIVE_LOOPBACK_0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Video standard → width/height/fps/interlaced ───────────────────── */
|
||||||
|
typedef struct { int width, height, fps_num, fps_den; int interlaced; } VideoInfo;
|
||||||
|
|
||||||
|
static VideoInfo video_info(VHD_VIDEOSTANDARD std, VHD_CLOCKDIVISOR div) {
|
||||||
|
int ntsc = (div == VHD_CLOCKDIV_1001);
|
||||||
|
switch (std) {
|
||||||
|
case VHD_VIDEOSTD_S274M_1080p_25Hz: return (VideoInfo){1920,1080,25,1,0};
|
||||||
|
case VHD_VIDEOSTD_S274M_1080p_30Hz: return (VideoInfo){1920,1080,ntsc?30000:30,ntsc?1001:1,0};
|
||||||
|
case VHD_VIDEOSTD_S274M_1080p_24Hz: return (VideoInfo){1920,1080,ntsc?24000:24,ntsc?1001:1,0};
|
||||||
|
case VHD_VIDEOSTD_S274M_1080p_50Hz: return (VideoInfo){1920,1080,50,1,0};
|
||||||
|
case VHD_VIDEOSTD_S274M_1080p_60Hz: return (VideoInfo){1920,1080,ntsc?60000:60,ntsc?1001:1,0};
|
||||||
|
case VHD_VIDEOSTD_S274M_1080psf_24Hz: return (VideoInfo){1920,1080,ntsc?24000:24,ntsc?1001:1,0};
|
||||||
|
case VHD_VIDEOSTD_S274M_1080psf_25Hz: return (VideoInfo){1920,1080,25,1,0};
|
||||||
|
case VHD_VIDEOSTD_S274M_1080psf_30Hz: return (VideoInfo){1920,1080,ntsc?30000:30,ntsc?1001:1,0};
|
||||||
|
case VHD_VIDEOSTD_S274M_1080i_50Hz: return (VideoInfo){1920,1080,25,1,1};
|
||||||
|
case VHD_VIDEOSTD_S274M_1080i_60Hz: return (VideoInfo){1920,1080,ntsc?30000:30,ntsc?1001:1,1};
|
||||||
|
case VHD_VIDEOSTD_S296M_720p_50Hz: return (VideoInfo){1280,720,50,1,0};
|
||||||
|
case VHD_VIDEOSTD_S296M_720p_60Hz: return (VideoInfo){1280,720,ntsc?60000:60,ntsc?1001:1,0};
|
||||||
|
case VHD_VIDEOSTD_S296M_720p_25Hz: return (VideoInfo){1280,720,25,1,0};
|
||||||
|
case VHD_VIDEOSTD_S296M_720p_30Hz: return (VideoInfo){1280,720,ntsc?30000:30,ntsc?1001:1,0};
|
||||||
|
case VHD_VIDEOSTD_S296M_720p_24Hz: return (VideoInfo){1280,720,ntsc?24000:24,ntsc?1001:1,0};
|
||||||
|
case VHD_VIDEOSTD_3840x2160p_24Hz: return (VideoInfo){3840,2160,ntsc?24000:24,ntsc?1001:1,0};
|
||||||
|
case VHD_VIDEOSTD_3840x2160p_25Hz: return (VideoInfo){3840,2160,25,1,0};
|
||||||
|
case VHD_VIDEOSTD_3840x2160p_30Hz: return (VideoInfo){3840,2160,ntsc?30000:30,ntsc?1001:1,0};
|
||||||
|
case VHD_VIDEOSTD_3840x2160p_50Hz: return (VideoInfo){3840,2160,50,1,0};
|
||||||
|
case VHD_VIDEOSTD_3840x2160p_60Hz: return (VideoInfo){3840,2160,ntsc?60000:60,ntsc?1001:1,0};
|
||||||
|
case VHD_VIDEOSTD_S259M_NTSC_480: return (VideoInfo){720,480,ntsc?30000:30,ntsc?1001:1,1};
|
||||||
|
default: return (VideoInfo){1920,1080,25,1,0};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Audio thread ────────────────────────────────────────────────────── */
|
||||||
|
typedef struct {
|
||||||
|
HANDLE board;
|
||||||
|
unsigned port;
|
||||||
|
ULONG video_std;
|
||||||
|
ULONG clock_div;
|
||||||
|
const char *fifo_path;
|
||||||
|
} AudioArgs;
|
||||||
|
|
||||||
|
static void *audio_thread(void *arg) {
|
||||||
|
AudioArgs *a = (AudioArgs *)arg;
|
||||||
|
|
||||||
|
HANDLE stream = NULL;
|
||||||
|
ULONG r = VHD_OpenStreamHandle(a->board, rx_streamtype(a->port),
|
||||||
|
VHD_SDI_STPROC_DISJOINED_ANC,
|
||||||
|
NULL, &stream, NULL);
|
||||||
|
if (r != VHDERR_NOERROR) {
|
||||||
|
fprintf(stderr, "[audio] VHD_OpenStreamHandle failed: %lu\n", r);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
VHD_SetStreamProperty(stream, VHD_SDI_SP_VIDEO_STANDARD, a->video_std);
|
||||||
|
VHD_SetStreamProperty(stream, VHD_SDI_SP_CLOCK_SYSTEM, a->clock_div);
|
||||||
|
VHD_SetStreamProperty(stream, VHD_CORE_SP_TRANSFER_SCHEME, VHD_TRANSFER_SLAVED);
|
||||||
|
|
||||||
|
/* Stereo pair, 16-bit, 48kHz on group 0 channel 0 */
|
||||||
|
ULONG max_samples = VHD_GetNbSamples((VHD_VIDEOSTANDARD)a->video_std,
|
||||||
|
(VHD_CLOCKDIVISOR)a->clock_div,
|
||||||
|
VHD_ASR_48000, 0);
|
||||||
|
ULONG block_size = VHD_GetBlockSize(VHD_AF_16, VHD_AM_STEREO);
|
||||||
|
ULONG buf_sz = (max_samples + 4) * block_size; /* +4 for 29.97 variation */
|
||||||
|
unsigned char *buf = calloc(1, buf_sz);
|
||||||
|
if (!buf) { VHD_CloseStreamHandle(stream); return NULL; }
|
||||||
|
|
||||||
|
VHD_AUDIOINFO ai;
|
||||||
|
memset(&ai, 0, sizeof(ai));
|
||||||
|
ai.pAudioGroups[0].pAudioChannels[0].Mode = VHD_AM_STEREO;
|
||||||
|
ai.pAudioGroups[0].pAudioChannels[0].BufferFormat = VHD_AF_16;
|
||||||
|
ai.pAudioGroups[0].pAudioChannels[0].pData = buf;
|
||||||
|
|
||||||
|
if (VHD_StartStream(stream) != VHDERR_NOERROR) {
|
||||||
|
free(buf); VHD_CloseStreamHandle(stream); return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Open FIFO for writing — blocks until FFmpeg opens the read end */
|
||||||
|
int fd = open(a->fifo_path, O_WRONLY);
|
||||||
|
if (fd < 0) {
|
||||||
|
fprintf(stderr, "[audio] open FIFO failed: %s\n", strerror(errno));
|
||||||
|
VHD_StopStream(stream); VHD_CloseStreamHandle(stream); free(buf);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
HANDLE slot = NULL;
|
||||||
|
while (!atomic_load(&g_stop)) {
|
||||||
|
r = VHD_LockSlotHandle(stream, &slot);
|
||||||
|
if (r == VHDERR_NOERROR) {
|
||||||
|
ai.pAudioGroups[0].pAudioChannels[0].DataSize = buf_sz;
|
||||||
|
if (VHD_SlotExtractAudio(slot, &ai) == VHDERR_NOERROR) {
|
||||||
|
ULONG sz = ai.pAudioGroups[0].pAudioChannels[0].DataSize;
|
||||||
|
if (sz > 0) write(fd, buf, sz);
|
||||||
|
}
|
||||||
|
VHD_UnlockSlotHandle(slot);
|
||||||
|
} else if (r != VHDERR_TIMEOUT) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close(fd);
|
||||||
|
VHD_StopStream(stream);
|
||||||
|
VHD_CloseStreamHandle(stream);
|
||||||
|
free(buf);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Main ────────────────────────────────────────────────────────────── */
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
unsigned device_id = 0;
|
||||||
|
unsigned port_id = 0;
|
||||||
|
int sig_timeout = 30;
|
||||||
|
const char *audio_pipe = NULL;
|
||||||
|
|
||||||
|
for (int i = 1; i < argc; i++) {
|
||||||
|
if (!strcmp(argv[i], "--device") && i+1 < argc) device_id = (unsigned)atoi(argv[++i]);
|
||||||
|
else if (!strcmp(argv[i], "--port") && i+1 < argc) port_id = (unsigned)atoi(argv[++i]);
|
||||||
|
else if (!strcmp(argv[i], "--audio-pipe") && i+1 < argc) audio_pipe = argv[++i];
|
||||||
|
else if (!strcmp(argv[i], "--signal-timeout") && i+1 < argc) sig_timeout = atoi(argv[++i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
signal(SIGINT, on_signal);
|
||||||
|
signal(SIGTERM, on_signal);
|
||||||
|
|
||||||
|
/* ── Init API ─────────────────────────────────────────────────── */
|
||||||
|
ULONG dll_ver, nb_boards;
|
||||||
|
if (VHD_GetApiInfo(&dll_ver, &nb_boards) != VHDERR_NOERROR) {
|
||||||
|
fprintf(stderr, "{\"error\":\"VHD_GetApiInfo failed\"}\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (device_id >= nb_boards) {
|
||||||
|
fprintf(stderr, "{\"error\":\"board %u not found (%lu detected)\"}\n", device_id, nb_boards);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Open board ───────────────────────────────────────────────── */
|
||||||
|
HANDLE board = NULL;
|
||||||
|
if (VHD_OpenBoardHandle(device_id, &board, NULL, 0) != VHDERR_NOERROR) {
|
||||||
|
fprintf(stderr, "{\"error\":\"VHD_OpenBoardHandle failed for board %u\"}\n", device_id);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Disable passive (relay) loopback so RX is live */
|
||||||
|
VHD_SetBoardProperty(board, loopback_prop(port_id), FALSE);
|
||||||
|
|
||||||
|
/* ── Wait for signal lock ──────────────────────────────────────── */
|
||||||
|
ULONG video_std = (ULONG)NB_VHD_VIDEOSTANDARDS;
|
||||||
|
struct timespec deadline;
|
||||||
|
clock_gettime(CLOCK_MONOTONIC, &deadline);
|
||||||
|
deadline.tv_sec += sig_timeout;
|
||||||
|
|
||||||
|
while (!atomic_load(&g_stop)) {
|
||||||
|
struct timespec now;
|
||||||
|
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||||
|
if (now.tv_sec > deadline.tv_sec ||
|
||||||
|
(now.tv_sec == deadline.tv_sec && now.tv_nsec >= deadline.tv_nsec)) break;
|
||||||
|
|
||||||
|
VHD_GetChannelProperty(board, VHD_RX_CHANNEL, port_id,
|
||||||
|
VHD_SDI_CP_VIDEO_STANDARD, &video_std);
|
||||||
|
if (video_std != (ULONG)NB_VHD_VIDEOSTANDARDS) break;
|
||||||
|
|
||||||
|
struct timespec ts = {0, 200000000L}; /* 200ms */
|
||||||
|
nanosleep(&ts, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (atomic_load(&g_stop) || video_std == (ULONG)NB_VHD_VIDEOSTANDARDS) {
|
||||||
|
fprintf(stderr,
|
||||||
|
"{\"error\":\"no signal on board %u port %u within %ds\"}\n",
|
||||||
|
device_id, port_id, sig_timeout);
|
||||||
|
VHD_CloseBoardHandle(board);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ULONG clock_div = VHD_CLOCKDIV_1;
|
||||||
|
VHD_GetChannelProperty(board, VHD_RX_CHANNEL, port_id,
|
||||||
|
VHD_SDI_CP_CLOCK_DIVISOR, &clock_div);
|
||||||
|
|
||||||
|
VideoInfo vi = video_info((VHD_VIDEOSTANDARD)video_std,
|
||||||
|
(VHD_CLOCKDIVISOR)clock_div);
|
||||||
|
|
||||||
|
/* ── Emit format JSON to stderr (one line, flushed) ─────────────── */
|
||||||
|
fprintf(stderr,
|
||||||
|
"{\"width\":%d,\"height\":%d,\"fps_num\":%d,\"fps_den\":%d,"
|
||||||
|
"\"interlaced\":%s,\"pix_fmt\":\"uyvy422\","
|
||||||
|
"\"audio_channels\":2,\"audio_rate\":48000,"
|
||||||
|
"\"device\":%u,\"port\":%u}\n",
|
||||||
|
vi.width, vi.height, vi.fps_num, vi.fps_den,
|
||||||
|
vi.interlaced ? "true" : "false",
|
||||||
|
device_id, port_id);
|
||||||
|
fflush(stderr);
|
||||||
|
|
||||||
|
/* ── Open video stream ───────────────────────────────────────────── */
|
||||||
|
HANDLE video_stream = NULL;
|
||||||
|
if (VHD_OpenStreamHandle(board, rx_streamtype(port_id),
|
||||||
|
VHD_SDI_STPROC_DISJOINED_VIDEO,
|
||||||
|
NULL, &video_stream, NULL) != VHDERR_NOERROR) {
|
||||||
|
fprintf(stderr, "{\"error\":\"VHD_OpenStreamHandle (video) failed\"}\n");
|
||||||
|
VHD_CloseBoardHandle(board);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
VHD_SetStreamProperty(video_stream, VHD_SDI_SP_VIDEO_STANDARD, video_std);
|
||||||
|
VHD_SetStreamProperty(video_stream, VHD_SDI_SP_CLOCK_SYSTEM, clock_div);
|
||||||
|
VHD_SetStreamProperty(video_stream, VHD_CORE_SP_TRANSFER_SCHEME, VHD_TRANSFER_SLAVED);
|
||||||
|
VHD_SetStreamProperty(video_stream, VHD_CORE_SP_SLOTS_QUEUE_DEPTH, 8);
|
||||||
|
|
||||||
|
/* ── Launch audio thread (FIFO open blocks until FFmpeg connects) ── */
|
||||||
|
pthread_t audio_tid = 0;
|
||||||
|
AudioArgs audio_args = { board, port_id, video_std, clock_div, audio_pipe };
|
||||||
|
if (audio_pipe) {
|
||||||
|
pthread_create(&audio_tid, NULL, audio_thread, &audio_args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Start video stream ──────────────────────────────────────────── */
|
||||||
|
if (VHD_StartStream(video_stream) != VHDERR_NOERROR) {
|
||||||
|
atomic_store(&g_stop, 1);
|
||||||
|
if (audio_tid) pthread_join(audio_tid, NULL);
|
||||||
|
VHD_CloseStreamHandle(video_stream);
|
||||||
|
VHD_CloseBoardHandle(board);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Video capture loop ──────────────────────────────────────────── */
|
||||||
|
HANDLE slot = NULL;
|
||||||
|
while (!atomic_load(&g_stop)) {
|
||||||
|
ULONG r = VHD_LockSlotHandle(video_stream, &slot);
|
||||||
|
if (r == VHDERR_NOERROR) {
|
||||||
|
BYTE *buf = NULL;
|
||||||
|
ULONG sz = 0;
|
||||||
|
if (VHD_GetSlotBuffer(slot, VHD_SDI_BT_VIDEO, &buf, &sz) == VHDERR_NOERROR) {
|
||||||
|
ULONG written = 0;
|
||||||
|
while (written < sz) {
|
||||||
|
ssize_t n = write(STDOUT_FILENO, buf + written, sz - written);
|
||||||
|
if (n <= 0) { atomic_store(&g_stop, 1); break; }
|
||||||
|
written += (ULONG)n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
VHD_UnlockSlotHandle(slot);
|
||||||
|
} else if (r != VHDERR_TIMEOUT) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Cleanup ─────────────────────────────────────────────────────── */
|
||||||
|
VHD_StopStream(video_stream);
|
||||||
|
VHD_CloseStreamHandle(video_stream);
|
||||||
|
if (audio_tid) pthread_join(audio_tid, NULL);
|
||||||
|
VHD_CloseBoardHandle(board);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add services/capture/deltacast-bridge/main.c
|
||||||
|
git commit -m "feat(capture): add deltacast-capture bridge binary source"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 3: Dockerfile — SDK extraction + bridge build + runtime
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `services/capture/Dockerfile`
|
||||||
|
|
||||||
|
The existing Dockerfile has three logical sections: FFmpeg build, runtime. We add two new stages before FFmpeg and patch the runtime stage.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Read the current Dockerfile**
|
||||||
|
|
||||||
|
Read `services/capture/Dockerfile` and verify it starts with `FROM debian:bookworm AS ffmpeg-builder`.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Prepend two new stages and patch runtime**
|
||||||
|
|
||||||
|
The full new Dockerfile:
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
# ── Stage 0: Extract Deltacast VideoMaster SDK ───────────────────────────
|
||||||
|
FROM debian:bookworm AS sdk-extractor
|
||||||
|
COPY videomaster-linux.x64-6.34.1-dev.tar.gz /tmp/
|
||||||
|
RUN mkdir -p /sdk && tar -xzf /tmp/videomaster-linux.x64-6.34.1-dev.tar.gz -C /sdk
|
||||||
|
|
||||||
|
# ── Stage 1: Build deltacast-capture bridge binary ───────────────────────
|
||||||
|
FROM debian:bookworm AS bridge-builder
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
build-essential cmake ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
COPY --from=sdk-extractor /sdk /sdk
|
||||||
|
COPY deltacast-bridge/ /bridge/
|
||||||
|
RUN cmake -S /bridge -B /bridge/build \
|
||||||
|
-DCMAKE_BUILD_TYPE=Release \
|
||||||
|
-DSDK_ROOT=/sdk \
|
||||||
|
&& cmake --build /bridge/build -j$(nproc)
|
||||||
|
|
||||||
|
# ── Stage 2: Build FFmpeg with DeckLink + NVENC (HEVC/H264) support ──────
|
||||||
|
# (unchanged — keep original content here)
|
||||||
|
FROM debian:bookworm AS ffmpeg-builder
|
||||||
|
# ... (rest of the existing ffmpeg-builder stage unchanged) ...
|
||||||
|
|
||||||
|
# ── Stage 3: Runtime image ───────────────────────────────────────────────
|
||||||
|
FROM node:20-bookworm
|
||||||
|
|
||||||
|
# Runtime deps for compiled ffmpeg libs (unchanged)
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
libx264-164 libx265-199 libvpx7 libopus0 libmp3lame0 \
|
||||||
|
libsrt1.5-openssl libzmq5 libstdc++6 libc++1 libc++abi1 \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy compiled ffmpeg/ffprobe (unchanged)
|
||||||
|
COPY --from=ffmpeg-builder /usr/local/bin/ffmpeg /usr/local/bin/ffmpeg
|
||||||
|
COPY --from=ffmpeg-builder /usr/local/bin/ffprobe /usr/local/bin/ffprobe
|
||||||
|
COPY --from=ffmpeg-builder /usr/local/lib/ /usr/local/lib/
|
||||||
|
|
||||||
|
# DeckLink runtime .so (unchanged)
|
||||||
|
COPY lib/libDeckLinkAPI.so /usr/lib/libDeckLinkAPI.so
|
||||||
|
COPY lib/libDeckLinkPreviewAPI.so /usr/lib/libDeckLinkPreviewAPI.so
|
||||||
|
|
||||||
|
# Deltacast bridge binary + SDK runtime libs
|
||||||
|
COPY --from=bridge-builder /bridge/build/deltacast-capture /usr/local/bin/deltacast-capture
|
||||||
|
COPY --from=sdk-extractor /sdk/lib/libvideomasterhd.so.6.34.1 /usr/local/lib/deltacast/
|
||||||
|
COPY --from=sdk-extractor /sdk/lib/libvideomasterhd_audio.so.6.34.1 /usr/local/lib/deltacast/
|
||||||
|
RUN ln -sf libvideomasterhd.so.6.34.1 /usr/local/lib/deltacast/libvideomasterhd.so.6 \
|
||||||
|
&& ln -sf libvideomasterhd.so.6.34.1 /usr/local/lib/deltacast/libvideomasterhd.so \
|
||||||
|
&& ln -sf libvideomasterhd_audio.so.6.34.1 /usr/local/lib/deltacast/libvideomasterhd_audio.so.6 \
|
||||||
|
&& ln -sf libvideomasterhd_audio.so.6.34.1 /usr/local/lib/deltacast/libvideomasterhd_audio.so \
|
||||||
|
&& ldconfig /usr/local/lib/deltacast \
|
||||||
|
&& ldconfig
|
||||||
|
|
||||||
|
RUN mkdir -p /live /growing
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install --omit=dev
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
EXPOSE 3001
|
||||||
|
CMD ["node", "src/index.js"]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Implementation note:** Edit the existing Dockerfile. Prepend the two new FROM stages (sdk-extractor, bridge-builder) before the existing `FROM debian:bookworm AS ffmpeg-builder` line. Then in the final runtime stage, add the Deltacast `COPY` and `RUN` lines after the DeckLink `.so` lines (before the `RUN mkdir -p /live /growing` line).
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add services/capture/Dockerfile
|
||||||
|
git commit -m "build(capture): add Deltacast SDK extraction and bridge build stages to Dockerfile"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 4: capture-manager.js — `readFirstStderrLine` helper
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `services/capture/src/capture-manager.js` (add helper near top, after imports)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add the helper function after the existing imports (after line 6 `import { v4 as uuidv4 } from 'uuid';`)**
|
||||||
|
|
||||||
|
```js
|
||||||
|
/**
|
||||||
|
* Reads the first line from a spawned process's stderr stream.
|
||||||
|
* Resolves with the parsed JSON object when the first '\n' arrives.
|
||||||
|
* Rejects if the process exits with a non-zero code before emitting a line,
|
||||||
|
* or if timeoutMs elapses.
|
||||||
|
*/
|
||||||
|
function readFirstStderrLine(proc, timeoutMs = 35_000) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let buf = '';
|
||||||
|
let settled = false;
|
||||||
|
const settle = (fn) => { if (settled) return; settled = true; fn(); };
|
||||||
|
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
settle(() => reject(new Error(`deltacast-capture: timed out waiting for format JSON after ${timeoutMs}ms`)));
|
||||||
|
}, timeoutMs);
|
||||||
|
|
||||||
|
proc.stderr.setEncoding('utf8');
|
||||||
|
proc.stderr.on('data', (chunk) => {
|
||||||
|
buf += chunk;
|
||||||
|
const nl = buf.indexOf('\n');
|
||||||
|
if (nl === -1) return;
|
||||||
|
const line = buf.slice(0, nl).trim();
|
||||||
|
clearTimeout(timer);
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(line);
|
||||||
|
if (parsed.error) {
|
||||||
|
settle(() => reject(new Error(`deltacast-capture: ${parsed.error}`)));
|
||||||
|
} else {
|
||||||
|
settle(() => resolve(parsed));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
settle(() => reject(new Error(`deltacast-capture: invalid JSON on stderr: ${line}`)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
proc.on('exit', (code) => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
settle(() => reject(new Error(`deltacast-capture: exited with code ${code} before emitting format JSON`)));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add services/capture/src/capture-manager.js
|
||||||
|
git commit -m "feat(capture): add readFirstStderrLine helper for deltacast bridge handshake"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 5: capture-manager.js — Deltacast `_buildInputArgs`
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `services/capture/src/capture-manager.js` — replace the `deltacast` branch of `_buildInputArgs` (currently lines 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
|
||||||
|
|
@ -0,0 +1,231 @@
|
||||||
|
# Deltacast SDI Capture — Design Spec
|
||||||
|
**Date:** 2026-06-01
|
||||||
|
**Status:** Approved
|
||||||
|
**Approach:** Bridge binary (Option B2)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
Dragonflight supports SDI ingest via Blackmagic DeckLink. Deltacast VideoMaster cards are a second hardware target. The VideoMaster SDK (v6.34.1) ships C++ headers and shared libraries but no FFmpeg demuxer plugin — there is no mainline FFmpeg `-f deltacast` input device. The `capture-manager.js` stub exists but falls back to a lavfi test card on all deployments.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
Write a small C++ bridge binary (`deltacast-capture`) using the VideoMaster C++ Wrapper SDK. The bridge:
|
||||||
|
1. Detects signal format on startup, writes one JSON line to stderr
|
||||||
|
2. Streams raw YUV video frames to stdout
|
||||||
|
3. Streams raw PCM audio to a named FIFO
|
||||||
|
|
||||||
|
`capture-manager.js` reads the JSON handshake, then spawns FFmpeg with `-f rawvideo -i pipe:0` (video from bridge stdout) and `-f s16le -i <fifo>` (audio from FIFO). The existing HEVC NVENC / ProRes encode pipeline is unchanged.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ capture container │
|
||||||
|
│ │
|
||||||
|
│ capture-manager.js │
|
||||||
|
│ │ │
|
||||||
|
│ ├─ spawn deltacast-capture --device 0 --port 0 │
|
||||||
|
│ │ --audio-pipe /tmp/dc-audio-{sessionId} │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ├─ stderr: JSON format line (one-time handshake) │
|
||||||
|
│ │ ├─ stdout: raw YUV frames (continuous) │
|
||||||
|
│ │ └─ FIFO: raw PCM audio (continuous) │
|
||||||
|
│ │ │
|
||||||
|
│ └─ spawn ffmpeg │
|
||||||
|
│ -f rawvideo -pix_fmt uyvy422 -s WxH -r FPS/1 │
|
||||||
|
│ -i pipe:0 ← piped from bridge stdout │
|
||||||
|
│ -f s16le -ar 48000 -ac <N> │
|
||||||
|
│ -i /tmp/dc-audio-{sessionId} │
|
||||||
|
│ <hevc_nvenc / prores / h264 encode args> │
|
||||||
|
│ <S3 pipe or growing-file output> │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### New files
|
||||||
|
- `services/capture/deltacast-bridge/CMakeLists.txt`
|
||||||
|
- `services/capture/deltacast-bridge/main.cpp`
|
||||||
|
|
||||||
|
### Modified files
|
||||||
|
- `services/capture/src/capture-manager.js` — `_buildInputArgs()` deltacast branch; `start()` and `stop()` bridge lifecycle
|
||||||
|
- `services/capture/Dockerfile` — SDK extraction stage, bridge build stage, runtime `.so` install
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The `deltacast-capture` Binary
|
||||||
|
|
||||||
|
### CLI
|
||||||
|
```
|
||||||
|
deltacast-capture
|
||||||
|
--device <N> Board index (0-based)
|
||||||
|
--port <N> RX port index (0-based)
|
||||||
|
--audio-pipe <path> Named FIFO path for PCM audio output
|
||||||
|
[--signal-timeout <sec=30>]
|
||||||
|
[--audio-groups <N=2>] Number of SDI audio groups (2 groups = 8 channels)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Startup sequence
|
||||||
|
1. `Board::open(device, loopback_restore_cb)`
|
||||||
|
2. Disable loopback on `port`
|
||||||
|
3. `board.sdi().open_stream(rx_streamtype(port), VHD_SDI_STPROC_DISJOINED_VIDEO)`
|
||||||
|
4. Poll `wait_for_input()` up to `--signal-timeout` seconds
|
||||||
|
5. On timeout → write `{"error":"no signal","device":N,"port":N}` to stderr, exit 1
|
||||||
|
6. Detect `video_standard`, `clock_divisor`, `interface` → map to width/height/fps/pix_fmt/interlaced
|
||||||
|
7. Write one JSON line to stderr (flushed):
|
||||||
|
```json
|
||||||
|
{"width":1920,"height":1080,"fps_num":25,"fps_den":1,"pix_fmt":"uyvy422","interlaced":false,"audio_channels":8,"audio_rate":48000,"device":0,"port":0}
|
||||||
|
```
|
||||||
|
8. Set queue depth = 8, `rx_stream.start()`
|
||||||
|
9. Capture loop: `pop_slot()` → write video buffer to stdout → extract audio → write PCM to FIFO (background thread)
|
||||||
|
10. SIGTERM/SIGINT → set stop flag → flush, close FIFO, close stream/board, exit 0
|
||||||
|
|
||||||
|
### Pixel format
|
||||||
|
Default: `uyvy422` (4:2:2 8-bit, `VHD_SDI_BUFTYPE_VIDEO`). 10-bit (`v210`) is a future follow-up via `--pix-fmt v210`.
|
||||||
|
|
||||||
|
### Audio
|
||||||
|
`sdi_slot.audio().extract(num_groups)` returns `std::vector<VHD_AUDIOGROUP>`. Samples are written to the FIFO as interleaved s16le PCM at 48000 Hz in a background thread so the video loop never blocks on audio consumers. Default `--audio-groups 2` yields 8 channels (standard embedded SDI stereo pairs 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
|
||||||
42
sdk/README.md
Normal file
42
sdk/README.md
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
# Capture-card SDK / driver file store
|
||||||
|
|
||||||
|
This directory holds the **proprietary, non-redistributable** vendor SDKs and
|
||||||
|
drivers used to enable SDI / NDI capture cards on cluster nodes.
|
||||||
|
|
||||||
|
> **INTERNAL ONLY.** These files are licensed by their respective vendors and
|
||||||
|
> must **not** be published, redistributed, or committed to any public mirror.
|
||||||
|
> This repository is private. Do not change that.
|
||||||
|
|
||||||
|
## Why these live in the repo
|
||||||
|
|
||||||
|
The cluster admin screen lets an operator install/update capture-card drivers on
|
||||||
|
a node from the web UI (no SSH). The node-agent spawns a one-shot privileged
|
||||||
|
container that bind-mounts this repository and runs
|
||||||
|
[`deploy/install-driver.sh <vendor>`](../deploy/install-driver.sh), which reads
|
||||||
|
the vendor files from `sdk/<vendor>/`. Because the install must work offline on
|
||||||
|
an isolated broadcast LAN, the binaries ship in-repo rather than being fetched at
|
||||||
|
install time.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
sdk/
|
||||||
|
README.md ← this file
|
||||||
|
blackmagic/ ← Blackmagic Desktop Video (DeckLink) .deb
|
||||||
|
aja/ ← AJA ntv2 driver source / installer
|
||||||
|
deltacast/ ← Deltacast VideoMaster installer
|
||||||
|
ndi/ ← NDI redistributable runtime libs
|
||||||
|
```
|
||||||
|
|
||||||
|
Each vendor directory has its own `README.md` listing **exactly** which files an
|
||||||
|
admin must drop in. A `.gitkeep` keeps the empty directory committed.
|
||||||
|
|
||||||
|
## Important
|
||||||
|
|
||||||
|
- **No binaries are committed by default.** The directory structure + READMEs
|
||||||
|
are the deliverable. An admin downloads the proprietary files from the vendor
|
||||||
|
(per their licence) and drops them in the matching `sdk/<vendor>/` directory.
|
||||||
|
- The install script **fails gracefully** with a clear message if the expected
|
||||||
|
file is absent — it never fabricates or downloads binaries.
|
||||||
|
- Target host OS for all install paths is **Ubuntu 22.04 LTS (jammy), x86_64**,
|
||||||
|
matching the cluster worker nodes.
|
||||||
0
sdk/aja/.gitkeep
Normal file
0
sdk/aja/.gitkeep
Normal file
31
sdk/aja/README.md
Normal file
31
sdk/aja/README.md
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# AJA NTV2 driver
|
||||||
|
|
||||||
|
Drop the **AJA NTV2** driver source/SDK archive for Linux into this directory.
|
||||||
|
|
||||||
|
## Required file
|
||||||
|
|
||||||
|
| File | Notes |
|
||||||
|
|------|-------|
|
||||||
|
| `ntv2sdk*linux*.zip` **or** `libajantv2*.tar.gz` | The NTV2 SDK / open-source `libajantv2` source tree containing `driver/linux/` with the kernel-module `Makefile` and `load_ajantv2` / `unload_ajantv2` scripts. |
|
||||||
|
|
||||||
|
Example names: `ntv2sdklinux_17.0.1.zip`, `libajantv2-17.0.1.tar.gz`
|
||||||
|
|
||||||
|
The installer reads the **newest** matching archive.
|
||||||
|
|
||||||
|
## Where to get it
|
||||||
|
|
||||||
|
AJA → Support → Software & firmware → *NTV2 SDK* (Linux), or the public
|
||||||
|
`aja-video/libajantv2` source release. Download the Linux SDK zip / source
|
||||||
|
tarball and copy it here unmodified.
|
||||||
|
|
||||||
|
## What the install script does
|
||||||
|
|
||||||
|
1. Ensures `linux-headers-$(uname -r)`, `build-essential` are present.
|
||||||
|
2. Extracts the archive into a scratch build dir.
|
||||||
|
3. Builds the `ajantv2` kernel module from `driver/linux` (`make`).
|
||||||
|
4. Installs the module under `/lib/modules/$(uname -r)/extra`, runs `depmod`,
|
||||||
|
`modprobe ajantv2` (falls back to the SDK's `load_ajantv2` script).
|
||||||
|
5. Verifies the `ajantv2` module is loaded.
|
||||||
|
|
||||||
|
A **reboot is not normally required**; the module loads immediately after build.
|
||||||
|
The script reports if a reboot is needed (e.g. an old in-tree module is wedged).
|
||||||
0
sdk/blackmagic/.gitkeep
Normal file
0
sdk/blackmagic/.gitkeep
Normal file
35
sdk/blackmagic/README.md
Normal file
35
sdk/blackmagic/README.md
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
# Blackmagic Desktop Video (DeckLink) driver
|
||||||
|
|
||||||
|
Drop the **Blackmagic Desktop Video** Debian package for **Ubuntu 22.04 (x86_64)**
|
||||||
|
into this directory.
|
||||||
|
|
||||||
|
## Required file
|
||||||
|
|
||||||
|
| File | Notes |
|
||||||
|
|------|-------|
|
||||||
|
| `desktopvideo_*_amd64.deb` | The `desktopvideo` package from the Desktop Video installer archive. Provides the `blackmagic` kernel module (built via DKMS) and the `DesktopVideoHelper` daemon. |
|
||||||
|
|
||||||
|
Example name: `desktopvideo_14.4.1a4_amd64.deb`
|
||||||
|
|
||||||
|
The installer reads the **newest** matching `desktopvideo_*_amd64.deb` if more
|
||||||
|
than one is present.
|
||||||
|
|
||||||
|
## Where to get it
|
||||||
|
|
||||||
|
Blackmagic Design → Support → *Desktop Video* (Linux). Download the
|
||||||
|
"Desktop Video x.y.z Linux" tarball, extract it, and copy the
|
||||||
|
`deb/<arch>/desktopvideo_*_amd64.deb` file here.
|
||||||
|
|
||||||
|
> Optional: `desktopvideo-gui_*_amd64.deb` is **not** required for headless
|
||||||
|
> capture and is not installed.
|
||||||
|
|
||||||
|
## What the install script does
|
||||||
|
|
||||||
|
1. Ensures `linux-headers-$(uname -r)` is present (needed for the DKMS build).
|
||||||
|
2. `apt-get install -y ./desktopvideo_*_amd64.deb` (pulls DKMS deps).
|
||||||
|
3. Triggers the DKMS build, `depmod`, `modprobe blackmagic`.
|
||||||
|
4. Restarts the `DesktopVideoHelper` daemon.
|
||||||
|
5. Verifies `/dev/blackmagic` appears.
|
||||||
|
|
||||||
|
A **reboot is usually not required** but a DKMS rebuild against a freshly
|
||||||
|
installed kernel may need one — the script reports this.
|
||||||
0
sdk/deltacast/.gitkeep
Normal file
0
sdk/deltacast/.gitkeep
Normal file
31
sdk/deltacast/README.md
Normal file
31
sdk/deltacast/README.md
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# Deltacast VideoMaster driver / SDK
|
||||||
|
|
||||||
|
Drop the **Deltacast VideoMaster** Linux installer into this directory.
|
||||||
|
|
||||||
|
## Required file
|
||||||
|
|
||||||
|
| File | Notes |
|
||||||
|
|------|-------|
|
||||||
|
| `VideoMaster*.run` **or** `VideoMaster*linux*.tar.gz` | The VideoMaster SDK + driver installer for Linux. Contains the `videomasterhd` kernel module sources and the `install.sh` driver installer. |
|
||||||
|
|
||||||
|
Example names: `VideoMaster-6.25.0.run`, `VideoMaster_6_25_Linux.tar.gz`
|
||||||
|
|
||||||
|
The installer reads the **newest** matching file.
|
||||||
|
|
||||||
|
## Where to get it
|
||||||
|
|
||||||
|
Deltacast → Products → *SDK* (<https://www.deltacast.tv/products/sdk>). Request
|
||||||
|
the VideoMaster Linux package (licence-gated) and copy the `.run` self-extractor
|
||||||
|
or the `.tar.gz` here unmodified.
|
||||||
|
|
||||||
|
## What the install script does
|
||||||
|
|
||||||
|
1. Ensures `linux-headers-$(uname -r)`, `build-essential`, `dkms` are present.
|
||||||
|
2. Runs the vendor installer:
|
||||||
|
- `.run` → executed with `--noexec --target <dir>` then its `install.sh`,
|
||||||
|
- `.tar.gz` → extracted, then its bundled `install.sh` is run.
|
||||||
|
3. Loads the Deltacast module (`modprobe videomasterhd` / vendor load script).
|
||||||
|
4. Verifies a `/dev/deltacast*` device node appears.
|
||||||
|
|
||||||
|
A **reboot may be required** after a first-time VideoMaster install (udev rules
|
||||||
|
+ firmware). The script reports this explicitly.
|
||||||
0
sdk/ndi/.gitkeep
Normal file
0
sdk/ndi/.gitkeep
Normal file
35
sdk/ndi/README.md
Normal file
35
sdk/ndi/README.md
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
# NDI redistributable runtime
|
||||||
|
|
||||||
|
Drop the **NDI runtime redistributable** shared libraries into this directory.
|
||||||
|
NDI has **no kernel module** — it is purely user-space shared libraries, so this
|
||||||
|
is the lowest-risk install (no DKMS, no reboot).
|
||||||
|
|
||||||
|
## Required files
|
||||||
|
|
||||||
|
| File | Notes |
|
||||||
|
|------|-------|
|
||||||
|
| `libndi.so.*` | The versioned NDI runtime shared object, e.g. `libndi.so.6`. **Required.** |
|
||||||
|
| `libndi.so` *(optional)* | Dev symlink. The installer recreates it if absent. |
|
||||||
|
|
||||||
|
You may instead drop the whole **NDI SDK / Advanced SDK** `lib/x86_64-linux-gnu/`
|
||||||
|
directory contents here; the installer copies every `libndi*.so*` it finds.
|
||||||
|
|
||||||
|
Example name: `libndi.so.6.1.1`
|
||||||
|
|
||||||
|
## Where to get it
|
||||||
|
|
||||||
|
NDI → Tools / SDK download (NDI 6 SDK or NDI Advanced SDK for Linux). The
|
||||||
|
runtime libs live under `lib/x86_64-linux-gnu/` in the SDK. Per the NDI licence
|
||||||
|
the runtime is redistributable **within your own product** only — keep it in this
|
||||||
|
private repo, do not publish it.
|
||||||
|
|
||||||
|
## What the install script does
|
||||||
|
|
||||||
|
1. Copies every `libndi*.so*` from here into `/opt/ndi-lib`.
|
||||||
|
2. Writes `/etc/ld.so.conf.d/ndi.conf` pointing at `/opt/ndi-lib` and runs
|
||||||
|
`ldconfig`.
|
||||||
|
3. Recreates the `libndi.so` → `libndi.so.<N>` dev symlink if missing.
|
||||||
|
4. Verifies `ldconfig -p | grep libndi` resolves.
|
||||||
|
|
||||||
|
**No reboot required.** Running processes that already loaded an old `libndi`
|
||||||
|
must be restarted to pick up the new version — the script notes this.
|
||||||
|
|
@ -1,4 +1,21 @@
|
||||||
# ── Stage 1: Build FFmpeg with DeckLink + NVENC (HEVC/H264) support ─────────
|
# ── Stage 0: Extract Deltacast VideoMaster SDK ───────────────────────────
|
||||||
|
FROM debian:bookworm AS sdk-extractor
|
||||||
|
COPY videomaster-linux.x64-6.34.1-dev.tar.gz /tmp/
|
||||||
|
RUN mkdir -p /sdk && tar -xzf /tmp/videomaster-linux.x64-6.34.1-dev.tar.gz -C /sdk
|
||||||
|
|
||||||
|
# ── Stage 1: Build deltacast-capture bridge binary ───────────────────────
|
||||||
|
FROM debian:bookworm AS bridge-builder
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
build-essential cmake ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
COPY --from=sdk-extractor /sdk /sdk
|
||||||
|
COPY deltacast-bridge/ /bridge/
|
||||||
|
RUN cmake -S /bridge -B /bridge/build \
|
||||||
|
-DCMAKE_BUILD_TYPE=Release \
|
||||||
|
-DSDK_ROOT=/sdk \
|
||||||
|
&& cmake --build /bridge/build -j$(nproc)
|
||||||
|
|
||||||
|
# ── Stage 2: Build FFmpeg with DeckLink + NVENC (HEVC/H264) support ─────────
|
||||||
# All-Intra HEVC NVENC is the master codec for growing-file ingest (see
|
# All-Intra HEVC NVENC is the master codec for growing-file ingest (see
|
||||||
# docs/design/2026-05-29-all-intra-hevc-ingest.md). This stage gets the
|
# docs/design/2026-05-29-all-intra-hevc-ingest.md). This stage gets the
|
||||||
# nv-codec-headers (header-only, no driver / no full CUDA toolkit needed)
|
# nv-codec-headers (header-only, no driver / no full CUDA toolkit needed)
|
||||||
|
|
@ -64,6 +81,34 @@ RUN ./configure \
|
||||||
RUN /usr/local/bin/ffmpeg -hide_banner -encoders 2>&1 | grep -E 'nvenc' \
|
RUN /usr/local/bin/ffmpeg -hide_banner -encoders 2>&1 | grep -E 'nvenc' \
|
||||||
|| (echo 'FATAL: nvenc encoders missing from ffmpeg build' && exit 1)
|
|| (echo 'FATAL: nvenc encoders missing from ffmpeg build' && exit 1)
|
||||||
|
|
||||||
|
# ── Stage 1b: Build bmx (raw2bmx / bmxtranswrap) from source ─────────────────
|
||||||
|
# bmx (bmxlib + libMXF + libMXF++) is the reference GROWING OP1a MXF writer. It
|
||||||
|
# writes a fresh IndexTableSegment (with an updated IndexDuration) into a new
|
||||||
|
# body partition at a periodic interval, so the recorded duration is readable —
|
||||||
|
# and INCREASES — from the header+index alone while the file is still being
|
||||||
|
# written (no footer needed). This is what makes the master a TRUE Premiere
|
||||||
|
# growing file. ffmpeg's MXF muxer cannot do this (its real duration/index lands
|
||||||
|
# only in the footer at av_write_trailer, so duration probes N/A until close).
|
||||||
|
#
|
||||||
|
# Debian/Ubuntu have no `bmxlib-tools` package (verified absent in bookworm), so
|
||||||
|
# we build from the BBC source. liburiparser/uuid/lzma/zlib/expat are the build
|
||||||
|
# deps; the runtime needs only libexpat1 + liburiparser1 + libuuid1 (added in
|
||||||
|
# the runtime stage below). Pinned to the bbc/bmx default branch (v1.6.x).
|
||||||
|
FROM debian:bookworm AS bmx-builder
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
build-essential cmake git ca-certificates pkg-config \
|
||||||
|
liburiparser-dev uuid-dev liblzma-dev zlib1g-dev libexpat1-dev \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
# Pin to a release tag so the produced soname (libMXF.so.1.6 etc.) stays stable
|
||||||
|
# for the COPY in the runtime stage. v1.6 is the BBC bmx series verified here.
|
||||||
|
RUN git clone --recursive --branch v1.6 https://github.com/bbc/bmx.git /bmx \
|
||||||
|
|| git clone --recursive https://github.com/bbc/bmx.git /bmx
|
||||||
|
WORKDIR /bmx/build
|
||||||
|
RUN cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/local .. \
|
||||||
|
&& make -j"$(nproc)" && make install && ldconfig
|
||||||
|
# Sanity-check: raw2bmx must run, otherwise the growing-MXF pipeline is broken.
|
||||||
|
RUN /usr/local/bin/raw2bmx -h >/dev/null 2>&1 && echo 'raw2bmx OK'
|
||||||
|
|
||||||
# ── Stage 2: Runtime image ───────────────────────────────────────────────────
|
# ── Stage 2: Runtime image ───────────────────────────────────────────────────
|
||||||
FROM node:20-bookworm
|
FROM node:20-bookworm
|
||||||
|
|
||||||
|
|
@ -75,6 +120,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
libx264-164 libx265-199 libvpx7 libopus0 libmp3lame0 \
|
libx264-164 libx265-199 libvpx7 libopus0 libmp3lame0 \
|
||||||
libsrt1.5-openssl libzmq5 libstdc++6 libc++1 libc++abi1 \
|
libsrt1.5-openssl libzmq5 libstdc++6 libc++1 libc++abi1 \
|
||||||
cifs-utils util-linux \
|
cifs-utils util-linux \
|
||||||
|
libexpat1 liburiparser1 libuuid1 \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Copy compiled ffmpeg/ffprobe
|
# Copy compiled ffmpeg/ffprobe
|
||||||
|
|
@ -85,7 +131,34 @@ COPY --from=ffmpeg-builder /usr/local/lib/ /usr/local/lib/
|
||||||
# DeckLink runtime .so
|
# DeckLink runtime .so
|
||||||
COPY lib/libDeckLinkAPI.so /usr/lib/libDeckLinkAPI.so
|
COPY lib/libDeckLinkAPI.so /usr/lib/libDeckLinkAPI.so
|
||||||
COPY lib/libDeckLinkPreviewAPI.so /usr/lib/libDeckLinkPreviewAPI.so
|
COPY lib/libDeckLinkPreviewAPI.so /usr/lib/libDeckLinkPreviewAPI.so
|
||||||
RUN ldconfig
|
|
||||||
|
# bmx (raw2bmx / bmxtranswrap / mxf2raw) — the growing OP1a MXF writer used for
|
||||||
|
# the edit-while-record master. Copy the built binaries + shared libs; runtime
|
||||||
|
# deps (libexpat1/liburiparser1/libuuid1) were installed above.
|
||||||
|
COPY --from=bmx-builder /usr/local/bin/raw2bmx /usr/local/bin/raw2bmx
|
||||||
|
COPY --from=bmx-builder /usr/local/bin/bmxtranswrap /usr/local/bin/bmxtranswrap
|
||||||
|
COPY --from=bmx-builder /usr/local/bin/mxf2raw /usr/local/bin/mxf2raw
|
||||||
|
COPY --from=bmx-builder /usr/local/lib/libMXF.so.1.6 /usr/local/lib/
|
||||||
|
COPY --from=bmx-builder /usr/local/lib/libMXF++.so.1.6 /usr/local/lib/
|
||||||
|
COPY --from=bmx-builder /usr/local/lib/libbmx.so.1.6 /usr/local/lib/
|
||||||
|
RUN cd /usr/local/lib \
|
||||||
|
&& ln -sf libMXF.so.1.6 libMXF.so.1 && ln -sf libMXF.so.1 libMXF.so \
|
||||||
|
&& ln -sf libMXF++.so.1.6 libMXF++.so.1 && ln -sf libMXF++.so.1 libMXF++.so \
|
||||||
|
&& ln -sf libbmx.so.1.6 libbmx.so.1 && ln -sf libbmx.so.1 libbmx.so \
|
||||||
|
&& ldconfig
|
||||||
|
# Verify raw2bmx resolves its libs and runs in the final image.
|
||||||
|
RUN raw2bmx -h >/dev/null 2>&1 && echo 'raw2bmx runtime OK'
|
||||||
|
|
||||||
|
# Deltacast bridge binary + SDK runtime libs
|
||||||
|
COPY --from=bridge-builder /bridge/build/deltacast-capture /usr/local/bin/deltacast-capture
|
||||||
|
COPY --from=sdk-extractor /sdk/lib/libvideomasterhd.so.6.34.1 /usr/local/lib/deltacast/
|
||||||
|
COPY --from=sdk-extractor /sdk/lib/libvideomasterhd_audio.so.6.34.1 /usr/local/lib/deltacast/
|
||||||
|
RUN ln -sf libvideomasterhd.so.6.34.1 /usr/local/lib/deltacast/libvideomasterhd.so.6 \
|
||||||
|
&& ln -sf libvideomasterhd.so.6.34.1 /usr/local/lib/deltacast/libvideomasterhd.so \
|
||||||
|
&& ln -sf libvideomasterhd_audio.so.6.34.1 /usr/local/lib/deltacast/libvideomasterhd_audio.so.6 \
|
||||||
|
&& ln -sf libvideomasterhd_audio.so.6.34.1 /usr/local/lib/deltacast/libvideomasterhd_audio.so \
|
||||||
|
&& ldconfig /usr/local/lib/deltacast \
|
||||||
|
&& ldconfig
|
||||||
|
|
||||||
# Mount points the recorder lifecycle expects to exist.
|
# Mount points the recorder lifecycle expects to exist.
|
||||||
# /live — HLS preview output (bound from host LIVE_DIR by node-agent)
|
# /live — HLS preview output (bound from host LIVE_DIR by node-agent)
|
||||||
|
|
|
||||||
37
services/capture/deltacast-bridge/CMakeLists.txt
Normal file
37
services/capture/deltacast-bridge/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
cmake_minimum_required(VERSION 3.16)
|
||||||
|
project(deltacast-bridge C)
|
||||||
|
set(CMAKE_C_STANDARD 17)
|
||||||
|
|
||||||
|
set(SDK_ROOT "/sdk" CACHE PATH "Path to extracted VideoMaster SDK")
|
||||||
|
|
||||||
|
# Primary binary: deltacast-bridge (shared multi-port daemon)
|
||||||
|
add_executable(deltacast-bridge main.c)
|
||||||
|
|
||||||
|
target_include_directories(deltacast-bridge PRIVATE
|
||||||
|
${SDK_ROOT}/include/videomaster
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_directories(deltacast-bridge PRIVATE
|
||||||
|
${SDK_ROOT}/lib
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(deltacast-bridge PRIVATE
|
||||||
|
videomasterhd
|
||||||
|
videomasterhd_audio
|
||||||
|
pthread
|
||||||
|
)
|
||||||
|
|
||||||
|
# Embed the SDK RPATH so the binary finds the .so at runtime
|
||||||
|
set_target_properties(deltacast-bridge PROPERTIES
|
||||||
|
INSTALL_RPATH "/usr/local/lib/deltacast"
|
||||||
|
BUILD_WITH_INSTALL_RPATH TRUE
|
||||||
|
)
|
||||||
|
|
||||||
|
# Compat symlink: deltacast-capture -> deltacast-bridge
|
||||||
|
# (node-agent and any legacy scripts that reference the old name still work)
|
||||||
|
add_custom_command(TARGET deltacast-bridge POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E create_symlink
|
||||||
|
$<TARGET_FILE:deltacast-bridge>
|
||||||
|
$<TARGET_FILE_DIR:deltacast-bridge>/deltacast-capture
|
||||||
|
COMMENT "Creating deltacast-capture compat symlink"
|
||||||
|
)
|
||||||
571
services/capture/deltacast-bridge/main.c
Normal file
571
services/capture/deltacast-bridge/main.c
Normal file
|
|
@ -0,0 +1,571 @@
|
||||||
|
/* services/capture/deltacast-bridge/main.c
|
||||||
|
*
|
||||||
|
* Deltacast VideoMaster SDI shared multi-port bridge daemon.
|
||||||
|
*
|
||||||
|
* Opens the board ONCE, opens RX streams for all requested ports, and
|
||||||
|
* writes each port's video/audio to named FIFOs in a shared directory.
|
||||||
|
* One reader thread + one audio thread per port run concurrently.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* deltacast-bridge --device <N> --ports <csv>
|
||||||
|
* [--video-pipe-dir /dev/shm/deltacast]
|
||||||
|
* [--audio-pipe-dir /dev/shm/deltacast]
|
||||||
|
* [--signal-timeout <sec>]
|
||||||
|
*
|
||||||
|
* Compat alias: --port <N> treated as --ports <N> (single port).
|
||||||
|
*
|
||||||
|
* For each port that acquires signal, emits one JSON line to stderr:
|
||||||
|
* {"port":N,"width":W,"height":H,"fps_num":N,"fps_den":D,
|
||||||
|
* "pix_fmt":"uyvy422","audio_rate":48000,"audio_channels":2}
|
||||||
|
*
|
||||||
|
* Runs until SIGTERM/SIGINT, then closes all streams and the board.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <stdatomic.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include "VideoMasterHD_Core.h"
|
||||||
|
#include "VideoMasterHD_Sdi.h"
|
||||||
|
#include "VideoMasterHD_Sdi_Audio.h"
|
||||||
|
|
||||||
|
/* ── 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 VHD_CORE_BP_PASSIVE_LOOPBACK_0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Video standard → width/height/fps/interlaced ───────────────────── */
|
||||||
|
typedef struct { int width, height, fps_num, fps_den; int interlaced; } VideoInfo;
|
||||||
|
|
||||||
|
static VideoInfo video_info(VHD_VIDEOSTANDARD std, VHD_CLOCKDIVISOR div) {
|
||||||
|
int ntsc = (div == VHD_CLOCKDIV_1001);
|
||||||
|
switch (std) {
|
||||||
|
case VHD_VIDEOSTD_S274M_1080p_25Hz: return (VideoInfo){1920,1080,25,1,0};
|
||||||
|
case VHD_VIDEOSTD_S274M_1080p_30Hz: return (VideoInfo){1920,1080,ntsc?30000:30,ntsc?1001:1,0};
|
||||||
|
case VHD_VIDEOSTD_S274M_1080p_24Hz: return (VideoInfo){1920,1080,ntsc?24000:24,ntsc?1001:1,0};
|
||||||
|
case VHD_VIDEOSTD_S274M_1080p_50Hz: return (VideoInfo){1920,1080,50,1,0};
|
||||||
|
case VHD_VIDEOSTD_S274M_1080p_60Hz: return (VideoInfo){1920,1080,ntsc?60000:60,ntsc?1001:1,0};
|
||||||
|
case VHD_VIDEOSTD_S274M_1080psf_24Hz: return (VideoInfo){1920,1080,ntsc?24000:24,ntsc?1001:1,0};
|
||||||
|
case VHD_VIDEOSTD_S274M_1080psf_25Hz: return (VideoInfo){1920,1080,25,1,0};
|
||||||
|
case VHD_VIDEOSTD_S274M_1080psf_30Hz: return (VideoInfo){1920,1080,ntsc?30000:30,ntsc?1001:1,0};
|
||||||
|
case VHD_VIDEOSTD_S274M_1080i_50Hz: return (VideoInfo){1920,1080,25,1,1};
|
||||||
|
case VHD_VIDEOSTD_S274M_1080i_60Hz: return (VideoInfo){1920,1080,ntsc?30000:30,ntsc?1001:1,1};
|
||||||
|
case VHD_VIDEOSTD_S296M_720p_50Hz: return (VideoInfo){1280,720,50,1,0};
|
||||||
|
case VHD_VIDEOSTD_S296M_720p_60Hz: return (VideoInfo){1280,720,ntsc?60000:60,ntsc?1001:1,0};
|
||||||
|
case VHD_VIDEOSTD_S296M_720p_25Hz: return (VideoInfo){1280,720,25,1,0};
|
||||||
|
case VHD_VIDEOSTD_S296M_720p_30Hz: return (VideoInfo){1280,720,ntsc?30000:30,ntsc?1001:1,0};
|
||||||
|
case VHD_VIDEOSTD_S296M_720p_24Hz: return (VideoInfo){1280,720,ntsc?24000:24,ntsc?1001:1,0};
|
||||||
|
case VHD_VIDEOSTD_3840x2160p_24Hz: return (VideoInfo){3840,2160,ntsc?24000:24,ntsc?1001:1,0};
|
||||||
|
case VHD_VIDEOSTD_3840x2160p_25Hz: return (VideoInfo){3840,2160,25,1,0};
|
||||||
|
case VHD_VIDEOSTD_3840x2160p_30Hz: return (VideoInfo){3840,2160,ntsc?30000:30,ntsc?1001:1,0};
|
||||||
|
case VHD_VIDEOSTD_3840x2160p_50Hz: return (VideoInfo){3840,2160,50,1,0};
|
||||||
|
case VHD_VIDEOSTD_3840x2160p_60Hz: return (VideoInfo){3840,2160,ntsc?60000:60,ntsc?1001:1,0};
|
||||||
|
case VHD_VIDEOSTD_S259M_NTSC_480: return (VideoInfo){720,480,ntsc?30000:30,ntsc?1001:1,1};
|
||||||
|
default: return (VideoInfo){1920,1080,25,1,0};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Write-all helper ─────────────────────────────────────────────────── */
|
||||||
|
static int write_all(int fd, const unsigned char *p, size_t len) {
|
||||||
|
size_t off = 0;
|
||||||
|
while (off < len) {
|
||||||
|
ssize_t n = write(fd, p + off, len - off);
|
||||||
|
if (n > 0) { off += (size_t)n; continue; }
|
||||||
|
if (n < 0 && errno == EINTR) continue;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Per-port state ───────────────────────────────────────────────────── */
|
||||||
|
typedef struct {
|
||||||
|
HANDLE board;
|
||||||
|
unsigned port;
|
||||||
|
unsigned device;
|
||||||
|
ULONG video_std;
|
||||||
|
ULONG clock_div;
|
||||||
|
VideoInfo vi;
|
||||||
|
char video_fifo[256];
|
||||||
|
char audio_fifo[256];
|
||||||
|
/* threads */
|
||||||
|
pthread_t video_tid;
|
||||||
|
pthread_t audio_tid;
|
||||||
|
/* streams (owned by threads, set before thread launch) */
|
||||||
|
HANDLE video_stream;
|
||||||
|
} PortState;
|
||||||
|
|
||||||
|
/* ── Audio thread ──────────────────────────────────────────────────────
|
||||||
|
*
|
||||||
|
* - 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;
|
||||||
|
}
|
||||||
|
fprintf(stderr, "[audio:%u] FIFO writer connected\n", ps->port);
|
||||||
|
|
||||||
|
/* Reset wall-clock baseline after potentially blocking on open(). */
|
||||||
|
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) {
|
||||||
|
r = VHD_LockSlotHandle(stream, &slot);
|
||||||
|
if (r == VHDERR_NOERROR) {
|
||||||
|
ai.pAudioGroups[0].pAudioChannels[0].DataSize = (ULONG)buf_sz;
|
||||||
|
if (VHD_SlotExtractAudio(slot, &ai) == VHDERR_NOERROR) {
|
||||||
|
ULONG sz = ai.pAudioGroups[0].pAudioChannels[0].DataSize;
|
||||||
|
if (sz > 0 && (size_t)sz <= buf_sz) out_bytes = (size_t)sz;
|
||||||
|
}
|
||||||
|
VHD_UnlockSlotHandle(slot);
|
||||||
|
} else if (r != VHDERR_TIMEOUT) {
|
||||||
|
fprintf(stderr, "[audio:%u] lock error %lu — degrading to silence\n",
|
||||||
|
ps->port, r);
|
||||||
|
VHD_StopStream(stream);
|
||||||
|
VHD_CloseStreamHandle(stream);
|
||||||
|
stream = NULL;
|
||||||
|
have_vhd_audio = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (out_bytes == 0) {
|
||||||
|
memset(buf, 0, tick_bytes);
|
||||||
|
out_bytes = tick_bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (write_all(fd, buf, out_bytes) < 0) {
|
||||||
|
/* EPIPE: ffmpeg reader on this port died (session stop/restart).
|
||||||
|
* Close and break to the outer loop which will reopen and block
|
||||||
|
* until the next ffmpeg reader connects.
|
||||||
|
* Do NOT set g_stop — other ports must keep running. */
|
||||||
|
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;
|
||||||
|
|
||||||
|
int fd = open(ps->video_fifo, O_WRONLY);
|
||||||
|
if (fd < 0) {
|
||||||
|
fprintf(stderr, "[video:%u] open FIFO failed: %s\n", ps->port, strerror(errno));
|
||||||
|
atomic_store(&g_port_stop[ps->port], 1);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
HANDLE slot = NULL;
|
||||||
|
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) {
|
||||||
|
if (write_all(fd, buf, sz) < 0) {
|
||||||
|
/* EPIPE on video: the capture sidecar for this port died.
|
||||||
|
* Stop only this port's threads — other ports unaffected. */
|
||||||
|
fprintf(stderr, "[video:%u] EPIPE — stopping port\n", ps->port);
|
||||||
|
atomic_store(&g_port_stop[ps->port], 1);
|
||||||
|
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);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close(fd);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Parse comma-separated port list ─────────────────────────────────── */
|
||||||
|
static int parse_ports(const char *csv, unsigned *ports, int max) {
|
||||||
|
int count = 0;
|
||||||
|
char buf[256];
|
||||||
|
strncpy(buf, csv, sizeof(buf) - 1);
|
||||||
|
buf[sizeof(buf) - 1] = '\0';
|
||||||
|
char *tok = strtok(buf, ",");
|
||||||
|
while (tok && count < max) {
|
||||||
|
ports[count++] = (unsigned)atoi(tok);
|
||||||
|
tok = strtok(NULL, ",");
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Main ─────────────────────────────────────────────────────────────── */
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
unsigned device_id = 0;
|
||||||
|
unsigned ports[MAX_PORTS] = {0};
|
||||||
|
int port_count = 0;
|
||||||
|
int sig_timeout = 30;
|
||||||
|
const char *video_pipe_dir = "/dev/shm/deltacast";
|
||||||
|
const char *audio_pipe_dir = "/dev/shm/deltacast";
|
||||||
|
|
||||||
|
for (int i = 1; i < argc; i++) {
|
||||||
|
if (!strcmp(argv[i], "--device") && i+1 < argc) {
|
||||||
|
device_id = (unsigned)atoi(argv[++i]);
|
||||||
|
} else if (!strcmp(argv[i], "--ports") && i+1 < argc) {
|
||||||
|
port_count = parse_ports(argv[++i], ports, MAX_PORTS);
|
||||||
|
} else if (!strcmp(argv[i], "--port") && i+1 < argc) {
|
||||||
|
/* single-port compat alias */
|
||||||
|
ports[0] = (unsigned)atoi(argv[++i]);
|
||||||
|
port_count = 1;
|
||||||
|
} else if (!strcmp(argv[i], "--video-pipe-dir") && i+1 < argc) {
|
||||||
|
video_pipe_dir = argv[++i];
|
||||||
|
} else if (!strcmp(argv[i], "--audio-pipe-dir") && i+1 < argc) {
|
||||||
|
audio_pipe_dir = argv[++i];
|
||||||
|
} else if (!strcmp(argv[i], "--signal-timeout") && i+1 < argc) {
|
||||||
|
sig_timeout = atoi(argv[++i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (port_count == 0) {
|
||||||
|
fprintf(stderr, "{\"error\":\"no ports specified — use --ports 0,1,2,...\"}\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
signal(SIGINT, on_signal);
|
||||||
|
signal(SIGTERM, on_signal);
|
||||||
|
signal(SIGPIPE, SIG_IGN);
|
||||||
|
|
||||||
|
/* ── Init API ────────────────────────────────────────────────────── */
|
||||||
|
ULONG dll_ver, nb_boards;
|
||||||
|
if (VHD_GetApiInfo(&dll_ver, &nb_boards) != VHDERR_NOERROR) {
|
||||||
|
fprintf(stderr, "{\"error\":\"VHD_GetApiInfo failed\"}\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (device_id >= nb_boards) {
|
||||||
|
fprintf(stderr, "{\"error\":\"board %u not found (%lu detected)\"}\n",
|
||||||
|
device_id, nb_boards);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Open board ONCE ─────────────────────────────────────────────── */
|
||||||
|
HANDLE board = NULL;
|
||||||
|
if (VHD_OpenBoardHandle(device_id, &board, NULL, 0) != VHDERR_NOERROR) {
|
||||||
|
fprintf(stderr, "{\"error\":\"VHD_OpenBoardHandle failed for board %u\"}\n", device_id);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
fprintf(stderr, "[board] opened board %u with %d port(s)\n", device_id, port_count);
|
||||||
|
|
||||||
|
/* Disable passive loopback for each requested port (ports 0-3 only in SDK). */
|
||||||
|
for (int pi = 0; pi < port_count; pi++) {
|
||||||
|
unsigned p = ports[pi];
|
||||||
|
if (p < 4) VHD_SetBoardProperty(board, loopback_prop(p), FALSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Wait for signal on all ports ───────────────────────────────── */
|
||||||
|
ULONG video_stds[MAX_PORTS] = {0};
|
||||||
|
ULONG clock_divs[MAX_PORTS] = {0};
|
||||||
|
int locked[MAX_PORTS] = {0};
|
||||||
|
|
||||||
|
for (int pi = 0; pi < port_count; pi++) {
|
||||||
|
video_stds[pi] = (ULONG)NB_VHD_VIDEOSTANDARDS;
|
||||||
|
clock_divs[pi] = VHD_CLOCKDIV_1;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct timespec deadline;
|
||||||
|
clock_gettime(CLOCK_MONOTONIC, &deadline);
|
||||||
|
deadline.tv_sec += sig_timeout;
|
||||||
|
|
||||||
|
while (!atomic_load(&g_stop)) {
|
||||||
|
struct timespec now;
|
||||||
|
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||||
|
if (now.tv_sec > deadline.tv_sec ||
|
||||||
|
(now.tv_sec == deadline.tv_sec && now.tv_nsec >= deadline.tv_nsec)) break;
|
||||||
|
|
||||||
|
int all_locked = 1;
|
||||||
|
for (int pi = 0; pi < port_count; pi++) {
|
||||||
|
if (locked[pi]) continue;
|
||||||
|
VHD_GetChannelProperty(board, VHD_RX_CHANNEL, ports[pi],
|
||||||
|
VHD_SDI_CP_VIDEO_STANDARD, &video_stds[pi]);
|
||||||
|
if (video_stds[pi] != (ULONG)NB_VHD_VIDEOSTANDARDS) {
|
||||||
|
VHD_GetChannelProperty(board, VHD_RX_CHANNEL, ports[pi],
|
||||||
|
VHD_SDI_CP_CLOCK_DIVISOR, &clock_divs[pi]);
|
||||||
|
locked[pi] = 1;
|
||||||
|
fprintf(stderr, "[board] port %u signal locked (std=%lu)\n",
|
||||||
|
ports[pi], video_stds[pi]);
|
||||||
|
} else {
|
||||||
|
all_locked = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (all_locked) break;
|
||||||
|
|
||||||
|
struct timespec ts = {0, 200000000L}; /* 200ms poll */
|
||||||
|
nanosleep(&ts, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Report results — continue with whatever locked, abort only if NONE locked. */
|
||||||
|
int any_locked = 0;
|
||||||
|
for (int pi = 0; pi < port_count; pi++) {
|
||||||
|
if (locked[pi]) { any_locked = 1; }
|
||||||
|
else {
|
||||||
|
fprintf(stderr,
|
||||||
|
"{\"error\":\"no signal on board %u port %u within %ds\"}\n",
|
||||||
|
device_id, ports[pi], sig_timeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!any_locked || atomic_load(&g_stop)) {
|
||||||
|
VHD_CloseBoardHandle(board);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Create FIFOs and open streams for each locked port ─────────── */
|
||||||
|
PortState ps[MAX_PORTS];
|
||||||
|
memset(ps, 0, sizeof(ps));
|
||||||
|
int active_count = 0;
|
||||||
|
|
||||||
|
/* 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);
|
||||||
|
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;
|
||||||
|
}
|
||||||
1692
services/capture/sdk/DeckLinkAPI.h
Normal file
1692
services/capture/sdk/DeckLinkAPI.h
Normal file
File diff suppressed because it is too large
Load diff
338
services/capture/sdk/DeckLinkAPIConfiguration.h
Normal file
338
services/capture/sdk/DeckLinkAPIConfiguration.h
Normal file
|
|
@ -0,0 +1,338 @@
|
||||||
|
/* -LICENSE-START-
|
||||||
|
** Copyright (c) 2026 Blackmagic Design
|
||||||
|
**
|
||||||
|
** Permission is hereby granted, free of charge, to any person or organization
|
||||||
|
** obtaining a copy of the software and accompanying documentation covered by
|
||||||
|
** this license (the "Software") to use, reproduce, display, distribute,
|
||||||
|
** execute, and transmit the Software, and to prepare derivative works of the
|
||||||
|
** Software, and to permit third-parties to whom the Software is furnished to
|
||||||
|
** do so, all subject to the following:
|
||||||
|
**
|
||||||
|
** The copyright notices in the Software and this entire statement, including
|
||||||
|
** the above license grant, this restriction and the following disclaimer,
|
||||||
|
** must be included in all copies of the Software, in whole or in part, and
|
||||||
|
** all derivative works of the Software, unless such copies or derivative
|
||||||
|
** works are solely in the form of machine-executable object code generated by
|
||||||
|
** a source language processor.
|
||||||
|
**
|
||||||
|
** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||||
|
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||||
|
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||||
|
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
** DEALINGS IN THE SOFTWARE.
|
||||||
|
** -LICENSE-END-
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -- AUTOMATICALLY GENERATED - DO NOT EDIT ---
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef BMD_DECKLINKAPICONFIGURATION_H
|
||||||
|
#define BMD_DECKLINKAPICONFIGURATION_H
|
||||||
|
|
||||||
|
|
||||||
|
#ifndef BMD_CONST
|
||||||
|
#if defined(_MSC_VER)
|
||||||
|
#define BMD_CONST __declspec(selectany) static const
|
||||||
|
#else
|
||||||
|
#define BMD_CONST static const
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef BMD_PUBLIC
|
||||||
|
#define BMD_PUBLIC
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Type Declarations
|
||||||
|
|
||||||
|
|
||||||
|
// Interface ID Declarations
|
||||||
|
|
||||||
|
BMD_CONST REFIID IID_IDeckLinkConfiguration = /* 5A68FFD4-1C12-4EDE-A6D2-45451D385FC1 */ { 0x5A,0x68,0xFF,0xD4,0x1C,0x12,0x4E,0xDE,0xA6,0xD2,0x45,0x45,0x1D,0x38,0x5F,0xC1 };
|
||||||
|
BMD_CONST REFIID IID_IDeckLinkEncoderConfiguration = /* 138050E5-C60A-4552-BF3F-0F358049327E */ { 0x13,0x80,0x50,0xE5,0xC6,0x0A,0x45,0x52,0xBF,0x3F,0x0F,0x35,0x80,0x49,0x32,0x7E };
|
||||||
|
|
||||||
|
/* Enum BMDDeckLinkConfigurationID - DeckLink Configuration ID */
|
||||||
|
|
||||||
|
typedef uint32_t BMDDeckLinkConfigurationID;
|
||||||
|
enum _BMDDeckLinkConfigurationID {
|
||||||
|
|
||||||
|
/* Serial port Flags */
|
||||||
|
|
||||||
|
bmdDeckLinkConfigSwapSerialRxTx = /* 'ssrt' */ 0x73737274,
|
||||||
|
|
||||||
|
/* Video Input/Output Integers */
|
||||||
|
|
||||||
|
bmdDeckLinkConfigHDMI3DPackingFormat = /* '3dpf' */ 0x33647066,
|
||||||
|
bmdDeckLinkConfigBypass = /* 'byps' */ 0x62797073,
|
||||||
|
bmdDeckLinkConfigClockTimingAdjustment = /* 'ctad' */ 0x63746164,
|
||||||
|
bmdDeckLinkConfigAudioMeterType = /* 'aumt' */ 0x61756D74,
|
||||||
|
|
||||||
|
/* Audio Input/Output Flags */
|
||||||
|
|
||||||
|
bmdDeckLinkConfigAnalogAudioConsumerLevels = /* 'aacl' */ 0x6161636C,
|
||||||
|
bmdDeckLinkConfigSwapHDMICh3AndCh4OnInput = /* 'hi34' */ 0x68693334,
|
||||||
|
bmdDeckLinkConfigSwapHDMICh3AndCh4OnOutput = /* 'ho34' */ 0x686F3334,
|
||||||
|
bmdDeckLinkConfigAnalogAudioOutputChannelsMutedByHeadphone = /* 'amhp' */ 0x616D6870,
|
||||||
|
bmdDeckLinkConfigAnalogAudioOutputChannelsMutedBySpeaker = /* 'amsp' */ 0x616D7370,
|
||||||
|
|
||||||
|
/* Video Output Flags */
|
||||||
|
|
||||||
|
bmdDeckLinkConfigFieldFlickerRemoval = /* 'fdfr' */ 0x66646672,
|
||||||
|
bmdDeckLinkConfigHD1080p24ToHD1080i5994Conversion = /* 'to59' */ 0x746F3539,
|
||||||
|
bmdDeckLinkConfig444SDIVideoOutput = /* '444o' */ 0x3434346F,
|
||||||
|
bmdDeckLinkConfigBlackVideoOutputDuringCapture = /* 'bvoc' */ 0x62766F63,
|
||||||
|
bmdDeckLinkConfigLowLatencyVideoOutput = /* 'llvo' */ 0x6C6C766F,
|
||||||
|
bmdDeckLinkConfigDownConversionOnAllAnalogOutput = /* 'caao' */ 0x6361616F,
|
||||||
|
bmdDeckLinkConfigSMPTELevelAOutput = /* 'smta' */ 0x736D7461,
|
||||||
|
bmdDeckLinkConfigRec2020Output = /* 'rec2' */ 0x72656332, // Ensure output is Rec.2020 colorspace
|
||||||
|
bmdDeckLinkConfigQuadLinkSDIVideoOutputSquareDivisionSplit = /* 'SDQS' */ 0x53445153,
|
||||||
|
bmdDeckLinkConfigOutput1080pAsPsF = /* 'pfpr' */ 0x70667072,
|
||||||
|
bmdDeckLinkConfigOutputValidateEDIDForDolbyVision = /* 'pred' */ 0x70726564,
|
||||||
|
bmdDeckLinkConfigExtendedDesktop = /* 'exdt' */ 0x65786474,
|
||||||
|
bmdDeckLinkConfigEthernetVideoOutputIP10 = /* 'IP10' */ 0x49503130,
|
||||||
|
|
||||||
|
/* Video Output Integers */
|
||||||
|
|
||||||
|
bmdDeckLinkConfigVideoOutputConnection = /* 'vocn' */ 0x766F636E,
|
||||||
|
bmdDeckLinkConfigVideoOutputConversionMode = /* 'vocm' */ 0x766F636D,
|
||||||
|
bmdDeckLinkConfigVideoOutputConversionColorspaceDestination = /* 'vccd' */ 0x76636364, // Parameter is of type BMDColorspace
|
||||||
|
bmdDeckLinkConfigVideoOutputConversionColorspaceSource = /* 'vccs' */ 0x76636373, // Parameter is of type BMDColorspace
|
||||||
|
bmdDeckLinkConfigAnalogVideoOutputFlags = /* 'avof' */ 0x61766F66,
|
||||||
|
bmdDeckLinkConfigReferenceInputTimingOffset = /* 'glot' */ 0x676C6F74,
|
||||||
|
bmdDeckLinkConfigReferenceOutputMode = /* 'glOm' */ 0x676C4F6D,
|
||||||
|
bmdDeckLinkConfigVideoOutputIdleOperation = /* 'voio' */ 0x766F696F,
|
||||||
|
bmdDeckLinkConfigDefaultVideoOutputMode = /* 'dvom' */ 0x64766F6D,
|
||||||
|
bmdDeckLinkConfigDefaultVideoOutputModeFlags = /* 'dvof' */ 0x64766F66,
|
||||||
|
bmdDeckLinkConfigSDIOutputLinkConfiguration = /* 'solc' */ 0x736F6C63,
|
||||||
|
bmdDeckLinkConfigHDMITimecodePacking = /* 'htpk' */ 0x6874706B,
|
||||||
|
bmdDeckLinkConfigPlaybackGroup = /* 'plgr' */ 0x706C6772,
|
||||||
|
|
||||||
|
/* Video Output Floats */
|
||||||
|
|
||||||
|
bmdDeckLinkConfigVideoOutputComponentLumaGain = /* 'oclg' */ 0x6F636C67,
|
||||||
|
bmdDeckLinkConfigVideoOutputComponentChromaBlueGain = /* 'occb' */ 0x6F636362,
|
||||||
|
bmdDeckLinkConfigVideoOutputComponentChromaRedGain = /* 'occr' */ 0x6F636372,
|
||||||
|
bmdDeckLinkConfigVideoOutputCompositeLumaGain = /* 'oilg' */ 0x6F696C67,
|
||||||
|
bmdDeckLinkConfigVideoOutputCompositeChromaGain = /* 'oicg' */ 0x6F696367,
|
||||||
|
bmdDeckLinkConfigVideoOutputSVideoLumaGain = /* 'oslg' */ 0x6F736C67,
|
||||||
|
bmdDeckLinkConfigVideoOutputSVideoChromaGain = /* 'oscg' */ 0x6F736367,
|
||||||
|
bmdDeckLinkConfigDolbyVisionCMVersion = /* 'dvvr' */ 0x64767672,
|
||||||
|
bmdDeckLinkConfigDolbyVisionMasterMinimumNits = /* 'mnnt' */ 0x6D6E6E74,
|
||||||
|
bmdDeckLinkConfigDolbyVisionMasterMaximumNits = /* 'mxnt' */ 0x6D786E74,
|
||||||
|
|
||||||
|
/* Video Input Flags */
|
||||||
|
|
||||||
|
bmdDeckLinkConfigVideoInputScanning = /* 'visc' */ 0x76697363, // Applicable to H264 Pro Recorder only
|
||||||
|
bmdDeckLinkConfigUseDedicatedLTCInput = /* 'dltc' */ 0x646C7463, // Use timecode from LTC input instead of SDI stream
|
||||||
|
bmdDeckLinkConfigSDIInput3DPayloadOverride = /* '3dds' */ 0x33646473,
|
||||||
|
bmdDeckLinkConfigCapture1080pAsPsF = /* 'cfpr' */ 0x63667072,
|
||||||
|
|
||||||
|
/* Video Input Integers */
|
||||||
|
|
||||||
|
bmdDeckLinkConfigVideoInputConnection = /* 'vicn' */ 0x7669636E,
|
||||||
|
bmdDeckLinkConfigAnalogVideoInputFlags = /* 'avif' */ 0x61766966,
|
||||||
|
bmdDeckLinkConfigVideoInputConversionMode = /* 'vicm' */ 0x7669636D,
|
||||||
|
bmdDeckLinkConfig32PulldownSequenceInitialTimecodeFrame = /* 'pdif' */ 0x70646966,
|
||||||
|
bmdDeckLinkConfigVANCSourceLine1Mapping = /* 'vsl1' */ 0x76736C31,
|
||||||
|
bmdDeckLinkConfigVANCSourceLine2Mapping = /* 'vsl2' */ 0x76736C32,
|
||||||
|
bmdDeckLinkConfigVANCSourceLine3Mapping = /* 'vsl3' */ 0x76736C33,
|
||||||
|
bmdDeckLinkConfigCapturePassThroughMode = /* 'cptm' */ 0x6370746D,
|
||||||
|
bmdDeckLinkConfigCaptureGroup = /* 'cpgr' */ 0x63706772,
|
||||||
|
bmdDeckLinkConfigHANCInputFilter1 = /* 'hif1' */ 0x68696631,
|
||||||
|
bmdDeckLinkConfigHANCInputFilter2 = /* 'hif2' */ 0x68696632,
|
||||||
|
bmdDeckLinkConfigHANCInputFilter3 = /* 'hif3' */ 0x68696633,
|
||||||
|
bmdDeckLinkConfigHANCInputFilter4 = /* 'hif4' */ 0x68696634,
|
||||||
|
|
||||||
|
/* Video Input Floats */
|
||||||
|
|
||||||
|
bmdDeckLinkConfigVideoInputComponentLumaGain = /* 'iclg' */ 0x69636C67,
|
||||||
|
bmdDeckLinkConfigVideoInputComponentChromaBlueGain = /* 'iccb' */ 0x69636362,
|
||||||
|
bmdDeckLinkConfigVideoInputComponentChromaRedGain = /* 'iccr' */ 0x69636372,
|
||||||
|
bmdDeckLinkConfigVideoInputCompositeLumaGain = /* 'iilg' */ 0x69696C67,
|
||||||
|
bmdDeckLinkConfigVideoInputCompositeChromaGain = /* 'iicg' */ 0x69696367,
|
||||||
|
bmdDeckLinkConfigVideoInputSVideoLumaGain = /* 'islg' */ 0x69736C67,
|
||||||
|
bmdDeckLinkConfigVideoInputSVideoChromaGain = /* 'iscg' */ 0x69736367,
|
||||||
|
|
||||||
|
/* Keying Integers */
|
||||||
|
|
||||||
|
bmdDeckLinkConfigInternalKeyingAncillaryDataSource = /* 'ikas' */ 0x696B6173,
|
||||||
|
|
||||||
|
/* Audio Input Flags */
|
||||||
|
|
||||||
|
bmdDeckLinkConfigMicrophonePhantomPower = /* 'mphp' */ 0x6D706870,
|
||||||
|
|
||||||
|
/* Audio Input Integers */
|
||||||
|
|
||||||
|
bmdDeckLinkConfigAudioInputConnection = /* 'aicn' */ 0x6169636E,
|
||||||
|
|
||||||
|
/* Audio Input Floats */
|
||||||
|
|
||||||
|
bmdDeckLinkConfigAnalogAudioInputScaleChannel1 = /* 'ais1' */ 0x61697331,
|
||||||
|
bmdDeckLinkConfigAnalogAudioInputScaleChannel2 = /* 'ais2' */ 0x61697332,
|
||||||
|
bmdDeckLinkConfigAnalogAudioInputScaleChannel3 = /* 'ais3' */ 0x61697333,
|
||||||
|
bmdDeckLinkConfigAnalogAudioInputScaleChannel4 = /* 'ais4' */ 0x61697334,
|
||||||
|
bmdDeckLinkConfigDigitalAudioInputScale = /* 'dais' */ 0x64616973,
|
||||||
|
bmdDeckLinkConfigMicrophoneInputGain = /* 'micg' */ 0x6D696367,
|
||||||
|
bmdDeckLinkConfigAudioOutputXLRDelayFrames = /* 'xdfr' */ 0x78646672,
|
||||||
|
|
||||||
|
/* Audio Output Integers */
|
||||||
|
|
||||||
|
bmdDeckLinkConfigAudioOutputAESAnalogSwitch = /* 'aoaa' */ 0x616F6161,
|
||||||
|
bmdDeckLinkConfigAudioOutputXLRDelayTime = /* 'xdms' */ 0x78646D73,
|
||||||
|
bmdDeckLinkConfigAudioOutputXLRDelayType = /* 'xdty' */ 0x78647479,
|
||||||
|
|
||||||
|
/* Audio Output Floats */
|
||||||
|
|
||||||
|
bmdDeckLinkConfigAnalogAudioOutputScaleChannel1 = /* 'aos1' */ 0x616F7331,
|
||||||
|
bmdDeckLinkConfigAnalogAudioOutputScaleChannel2 = /* 'aos2' */ 0x616F7332,
|
||||||
|
bmdDeckLinkConfigAnalogAudioOutputScaleChannel3 = /* 'aos3' */ 0x616F7333,
|
||||||
|
bmdDeckLinkConfigAnalogAudioOutputScaleChannel4 = /* 'aos4' */ 0x616F7334,
|
||||||
|
bmdDeckLinkConfigDigitalAudioOutputScale = /* 'daos' */ 0x64616F73,
|
||||||
|
bmdDeckLinkConfigHeadphoneVolume = /* 'hvol' */ 0x68766F6C,
|
||||||
|
bmdDeckLinkConfigSpeakerVolume = /* 'svol' */ 0x73766F6C,
|
||||||
|
|
||||||
|
/* Ethernet Flags */
|
||||||
|
|
||||||
|
bmdDeckLinkConfigEthernetPTPFollowerOnly = /* 'PTPf' */ 0x50545066,
|
||||||
|
bmdDeckLinkConfigEthernetPTPUseUDPEncapsulation = /* 'PTPU' */ 0x50545055,
|
||||||
|
bmdDeckLinkConfigEthernetUseManualNMOSRegistry = /* 'nmrp' */ 0x6E6D7270,
|
||||||
|
|
||||||
|
/* Ethernet Integers */
|
||||||
|
|
||||||
|
bmdDeckLinkConfigEthernetPTPPriority1 = /* 'PTP1' */ 0x50545031,
|
||||||
|
bmdDeckLinkConfigEthernetPTPPriority2 = /* 'PTP2' */ 0x50545032,
|
||||||
|
bmdDeckLinkConfigEthernetPTPDomain = /* 'PTPD' */ 0x50545044,
|
||||||
|
bmdDeckLinkConfigEthernetPTPLogAnnounceInterval = /* 'PTPA' */ 0x50545041,
|
||||||
|
|
||||||
|
/* Ethernet Strings */
|
||||||
|
|
||||||
|
bmdDeckLinkConfigEthernetAudioOutputChannelOrder = /* 'caco' */ 0x6361636F,
|
||||||
|
bmdDeckLinkConfigEthernetNMOSRegistryAddress = /* 'nmre' */ 0x6E6D7265,
|
||||||
|
|
||||||
|
/* Parameterized Ethernet Flags */
|
||||||
|
|
||||||
|
bmdDeckLinkConfigParamEthernetUseDHCP = /* 'DHCP' */ 0x44484350,
|
||||||
|
|
||||||
|
/* Parameterized Ethernet Strings */
|
||||||
|
|
||||||
|
bmdDeckLinkConfigParamEthernetStaticLocalIPAddress = /* 'nsip' */ 0x6E736970,
|
||||||
|
bmdDeckLinkConfigParamEthernetStaticSubnetMask = /* 'nssm' */ 0x6E73736D,
|
||||||
|
bmdDeckLinkConfigParamEthernetStaticGatewayIPAddress = /* 'nsgw' */ 0x6E736777,
|
||||||
|
bmdDeckLinkConfigParamEthernetStaticPrimaryDNS = /* 'nspd' */ 0x6E737064,
|
||||||
|
bmdDeckLinkConfigParamEthernetStaticSecondaryDNS = /* 'nssd' */ 0x6E737364,
|
||||||
|
bmdDeckLinkConfigParamEthernetVideoOutputAddress = /* 'noav' */ 0x6E6F6176,
|
||||||
|
bmdDeckLinkConfigParamEthernetAudioOutputAddress = /* 'noaa' */ 0x6E6F6161,
|
||||||
|
bmdDeckLinkConfigParamEthernetAncillaryOutputAddress = /* 'noaA' */ 0x6E6F6141,
|
||||||
|
|
||||||
|
/* Device Information Strings */
|
||||||
|
|
||||||
|
bmdDeckLinkConfigDeviceInformationLabel = /* 'dila' */ 0x64696C61,
|
||||||
|
bmdDeckLinkConfigDeviceInformationSerialNumber = /* 'disn' */ 0x6469736E,
|
||||||
|
bmdDeckLinkConfigDeviceInformationCompany = /* 'dico' */ 0x6469636F,
|
||||||
|
bmdDeckLinkConfigDeviceInformationPhone = /* 'diph' */ 0x64697068,
|
||||||
|
bmdDeckLinkConfigDeviceInformationEmail = /* 'diem' */ 0x6469656D,
|
||||||
|
bmdDeckLinkConfigDeviceInformationDate = /* 'dida' */ 0x64696461,
|
||||||
|
|
||||||
|
/* Deck Control Integers */
|
||||||
|
|
||||||
|
bmdDeckLinkConfigDeckControlConnection = /* 'dcco' */ 0x6463636F,
|
||||||
|
|
||||||
|
/* UI/UX Integers */
|
||||||
|
|
||||||
|
bmdDeckLinkConfigDisplayLanguage = /* 'lang' */ 0x6C616E67
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Enum BMDDeckLinkEncoderConfigurationID - DeckLink Encoder Configuration ID */
|
||||||
|
|
||||||
|
typedef uint32_t BMDDeckLinkEncoderConfigurationID;
|
||||||
|
enum _BMDDeckLinkEncoderConfigurationID {
|
||||||
|
|
||||||
|
/* Video Encoder Integers */
|
||||||
|
|
||||||
|
bmdDeckLinkEncoderConfigPreferredBitDepth = /* 'epbr' */ 0x65706272,
|
||||||
|
bmdDeckLinkEncoderConfigFrameCodingMode = /* 'efcm' */ 0x6566636D,
|
||||||
|
|
||||||
|
/* HEVC/H.265 Encoder Integers */
|
||||||
|
|
||||||
|
bmdDeckLinkEncoderConfigH265TargetBitrate = /* 'htbr' */ 0x68746272,
|
||||||
|
|
||||||
|
/* DNxHR/DNxHD Compression ID */
|
||||||
|
|
||||||
|
bmdDeckLinkEncoderConfigDNxHRCompressionID = /* 'dcid' */ 0x64636964,
|
||||||
|
|
||||||
|
/* DNxHR/DNxHD Level */
|
||||||
|
|
||||||
|
bmdDeckLinkEncoderConfigDNxHRLevel = /* 'dlev' */ 0x646C6576,
|
||||||
|
|
||||||
|
/* Encoded Sample Decriptions */
|
||||||
|
|
||||||
|
bmdDeckLinkEncoderConfigMPEG4SampleDescription = /* 'stsE' */ 0x73747345, // Full MPEG4 sample description (aka SampleEntry of an 'stsd' atom-box). Useful for MediaFoundation, QuickTime, MKV and more
|
||||||
|
bmdDeckLinkEncoderConfigMPEG4CodecSpecificDesc = /* 'esds' */ 0x65736473 // Sample description extensions only (atom stream, each with size and fourCC header). Useful for AVFoundation, VideoToolbox, MKV and more
|
||||||
|
};
|
||||||
|
|
||||||
|
#if defined(__cplusplus)
|
||||||
|
|
||||||
|
// Forward Declarations
|
||||||
|
|
||||||
|
class IDeckLinkConfiguration;
|
||||||
|
class IDeckLinkEncoderConfiguration;
|
||||||
|
|
||||||
|
/* Interface IDeckLinkConfiguration - DeckLink Configuration interface */
|
||||||
|
|
||||||
|
class BMD_PUBLIC IDeckLinkConfiguration : public IUnknown
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual HRESULT SetFlag (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ bool value) = 0;
|
||||||
|
virtual HRESULT GetFlag (/* in */ BMDDeckLinkConfigurationID cfgID, /* out */ bool* value) = 0;
|
||||||
|
virtual HRESULT SetInt (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ int64_t value) = 0;
|
||||||
|
virtual HRESULT GetInt (/* in */ BMDDeckLinkConfigurationID cfgID, /* out */ int64_t* value) = 0;
|
||||||
|
virtual HRESULT SetFloat (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ double value) = 0;
|
||||||
|
virtual HRESULT GetFloat (/* in */ BMDDeckLinkConfigurationID cfgID, /* out */ double* value) = 0;
|
||||||
|
virtual HRESULT SetString (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ const char* value) = 0;
|
||||||
|
virtual HRESULT GetString (/* in */ BMDDeckLinkConfigurationID cfgID, /* out */ const char** value) = 0;
|
||||||
|
virtual HRESULT SetFlagWithParam (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ uint64_t param, /* in */ bool value) = 0;
|
||||||
|
virtual HRESULT GetFlagWithParam (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ uint64_t param, /* out */ bool* value) = 0;
|
||||||
|
virtual HRESULT SetIntWithParam (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ uint64_t param, /* in */ int64_t value) = 0;
|
||||||
|
virtual HRESULT GetIntWithParam (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ uint64_t param, /* out */ int64_t* value) = 0;
|
||||||
|
virtual HRESULT SetFloatWithParam (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ uint64_t param, /* in */ double value) = 0;
|
||||||
|
virtual HRESULT GetFloatWithParam (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ uint64_t param, /* out */ double* value) = 0;
|
||||||
|
virtual HRESULT SetStringWithParam (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ uint64_t param, /* in */ const char* value) = 0;
|
||||||
|
virtual HRESULT GetStringWithParam (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ uint64_t param, /* out */ const char** value) = 0;
|
||||||
|
virtual HRESULT WriteConfigurationToPreferences (void) = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual ~IDeckLinkConfiguration () {} // call Release method to drop reference count
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Interface IDeckLinkEncoderConfiguration - DeckLink Encoder Configuration interface. Obtained from IDeckLinkEncoderInput */
|
||||||
|
|
||||||
|
class BMD_PUBLIC IDeckLinkEncoderConfiguration : public IUnknown
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual HRESULT SetFlag (/* in */ BMDDeckLinkEncoderConfigurationID cfgID, /* in */ bool value) = 0;
|
||||||
|
virtual HRESULT GetFlag (/* in */ BMDDeckLinkEncoderConfigurationID cfgID, /* out */ bool* value) = 0;
|
||||||
|
virtual HRESULT SetInt (/* in */ BMDDeckLinkEncoderConfigurationID cfgID, /* in */ int64_t value) = 0;
|
||||||
|
virtual HRESULT GetInt (/* in */ BMDDeckLinkEncoderConfigurationID cfgID, /* out */ int64_t* value) = 0;
|
||||||
|
virtual HRESULT SetFloat (/* in */ BMDDeckLinkEncoderConfigurationID cfgID, /* in */ double value) = 0;
|
||||||
|
virtual HRESULT GetFloat (/* in */ BMDDeckLinkEncoderConfigurationID cfgID, /* out */ double* value) = 0;
|
||||||
|
virtual HRESULT SetString (/* in */ BMDDeckLinkEncoderConfigurationID cfgID, /* in */ const char* value) = 0;
|
||||||
|
virtual HRESULT GetString (/* in */ BMDDeckLinkEncoderConfigurationID cfgID, /* out */ const char** value) = 0;
|
||||||
|
virtual HRESULT GetBytes (/* in */ BMDDeckLinkEncoderConfigurationID cfgID, /* out */ void* buffer /* optional */, /* in, out */ uint32_t* bufferSize) = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual ~IDeckLinkEncoderConfiguration () {} // call Release method to drop reference count
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Functions */
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#endif /* defined(__cplusplus) */
|
||||||
|
#endif /* defined(BMD_DECKLINKAPICONFIGURATION_H) */
|
||||||
84
services/capture/sdk/DeckLinkAPIConfiguration_v10_11.h
Normal file
84
services/capture/sdk/DeckLinkAPIConfiguration_v10_11.h
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
/* -LICENSE-START-
|
||||||
|
** Copyright (c) 2017 Blackmagic Design
|
||||||
|
**
|
||||||
|
** Permission is hereby granted, free of charge, to any person or organization
|
||||||
|
** obtaining a copy of the software and accompanying documentation (the
|
||||||
|
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||||
|
** and transmit the Software, and to prepare derivative works of the Software,
|
||||||
|
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||||
|
** accordance with:
|
||||||
|
**
|
||||||
|
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||||
|
** Agreement for the Software Development Kit ("EULA") available at
|
||||||
|
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||||
|
**
|
||||||
|
** (2) if the Software is obtained from any third party, such licensing terms
|
||||||
|
** as notified by that third party,
|
||||||
|
**
|
||||||
|
** and all subject to the following:
|
||||||
|
**
|
||||||
|
** (3) the copyright notices in the Software and this entire statement,
|
||||||
|
** including the above license grant, this restriction and the following
|
||||||
|
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||||
|
** part, and all derivative works of the Software, unless such copies or
|
||||||
|
** derivative works are solely in the form of machine-executable object code
|
||||||
|
** generated by a source language processor.
|
||||||
|
**
|
||||||
|
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||||
|
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||||
|
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||||
|
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
** DEALINGS IN THE SOFTWARE.
|
||||||
|
**
|
||||||
|
** A copy of the Software is available free of charge at
|
||||||
|
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||||
|
**
|
||||||
|
** -LICENSE-END-
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef BMD_DECKLINKAPICONFIGURATION_v10_11_H
|
||||||
|
#define BMD_DECKLINKAPICONFIGURATION_v10_11_H
|
||||||
|
|
||||||
|
#include "DeckLinkAPIConfiguration.h"
|
||||||
|
|
||||||
|
// Interface ID Declarations
|
||||||
|
|
||||||
|
BMD_CONST REFIID IID_IDeckLinkConfiguration_v10_11 = /* EF90380B-4AE5-4346-9077-E288E149F129 */ {0xEF,0x90,0x38,0x0B,0x4A,0xE5,0x43,0x46,0x90,0x77,0xE2,0x88,0xE1,0x49,0xF1,0x29};
|
||||||
|
|
||||||
|
/* Enum BMDDeckLinkConfigurationID_v10_11 - DeckLink Configuration ID */
|
||||||
|
|
||||||
|
typedef uint32_t BMDDeckLinkConfigurationID_v10_11;
|
||||||
|
enum _BMDDeckLinkConfigurationID_v10_11 {
|
||||||
|
|
||||||
|
/* Video Input/Output Integers */
|
||||||
|
|
||||||
|
bmdDeckLinkConfigDuplexMode_v10_11 = /* 'dupx' */ 0x64757078,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Forward Declarations
|
||||||
|
|
||||||
|
class IDeckLinkConfiguration_v10_11;
|
||||||
|
|
||||||
|
/* Interface IDeckLinkConfiguration_v10_11 - DeckLink Configuration interface */
|
||||||
|
|
||||||
|
class BMD_PUBLIC IDeckLinkConfiguration_v10_11 : public IUnknown
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual HRESULT SetFlag (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ bool value) = 0;
|
||||||
|
virtual HRESULT GetFlag (/* in */ BMDDeckLinkConfigurationID cfgID, /* out */ bool *value) = 0;
|
||||||
|
virtual HRESULT SetInt (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ int64_t value) = 0;
|
||||||
|
virtual HRESULT GetInt (/* in */ BMDDeckLinkConfigurationID cfgID, /* out */ int64_t *value) = 0;
|
||||||
|
virtual HRESULT SetFloat (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ double value) = 0;
|
||||||
|
virtual HRESULT GetFloat (/* in */ BMDDeckLinkConfigurationID cfgID, /* out */ double *value) = 0;
|
||||||
|
virtual HRESULT SetString (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ const char *value) = 0;
|
||||||
|
virtual HRESULT GetString (/* in */ BMDDeckLinkConfigurationID cfgID, /* out */ const char **value) = 0;
|
||||||
|
virtual HRESULT WriteConfigurationToPreferences (void) = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual ~IDeckLinkConfiguration_v10_11 () {} // call Release method to drop reference count
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
#endif /* defined(BMD_DECKLINKAPICONFIGURATION_v10_11_H) */
|
||||||
73
services/capture/sdk/DeckLinkAPIConfiguration_v10_2.h
Normal file
73
services/capture/sdk/DeckLinkAPIConfiguration_v10_2.h
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
/* -LICENSE-START-
|
||||||
|
** Copyright (c) 2014 Blackmagic Design
|
||||||
|
**
|
||||||
|
** Permission is hereby granted, free of charge, to any person or organization
|
||||||
|
** obtaining a copy of the software and accompanying documentation (the
|
||||||
|
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||||
|
** and transmit the Software, and to prepare derivative works of the Software,
|
||||||
|
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||||
|
** accordance with:
|
||||||
|
**
|
||||||
|
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||||
|
** Agreement for the Software Development Kit ("EULA") available at
|
||||||
|
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||||
|
**
|
||||||
|
** (2) if the Software is obtained from any third party, such licensing terms
|
||||||
|
** as notified by that third party,
|
||||||
|
**
|
||||||
|
** and all subject to the following:
|
||||||
|
**
|
||||||
|
** (3) the copyright notices in the Software and this entire statement,
|
||||||
|
** including the above license grant, this restriction and the following
|
||||||
|
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||||
|
** part, and all derivative works of the Software, unless such copies or
|
||||||
|
** derivative works are solely in the form of machine-executable object code
|
||||||
|
** generated by a source language processor.
|
||||||
|
**
|
||||||
|
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||||
|
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||||
|
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||||
|
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
** DEALINGS IN THE SOFTWARE.
|
||||||
|
**
|
||||||
|
** A copy of the Software is available free of charge at
|
||||||
|
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||||
|
**
|
||||||
|
** -LICENSE-END-
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef BMD_DECKLINKAPICONFIGURATION_v10_2_H
|
||||||
|
#define BMD_DECKLINKAPICONFIGURATION_v10_2_H
|
||||||
|
|
||||||
|
#include "DeckLinkAPIConfiguration.h"
|
||||||
|
|
||||||
|
// Interface ID Declarations
|
||||||
|
|
||||||
|
BMD_CONST REFIID IID_IDeckLinkConfiguration_v10_2 = /* C679A35B-610C-4D09-B748-1D0478100FC0 */ {0xC6,0x79,0xA3,0x5B,0x61,0x0C,0x4D,0x09,0xB7,0x48,0x1D,0x04,0x78,0x10,0x0F,0xC0};
|
||||||
|
|
||||||
|
// Forward Declarations
|
||||||
|
|
||||||
|
class IDeckLinkConfiguration_v10_2;
|
||||||
|
|
||||||
|
/* Interface IDeckLinkConfiguration_v10_2 - DeckLink Configuration interface */
|
||||||
|
|
||||||
|
class BMD_PUBLIC IDeckLinkConfiguration_v10_2 : public IUnknown
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual HRESULT SetFlag (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ bool value) = 0;
|
||||||
|
virtual HRESULT GetFlag (/* in */ BMDDeckLinkConfigurationID cfgID, /* out */ bool *value) = 0;
|
||||||
|
virtual HRESULT SetInt (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ int64_t value) = 0;
|
||||||
|
virtual HRESULT GetInt (/* in */ BMDDeckLinkConfigurationID cfgID, /* out */ int64_t *value) = 0;
|
||||||
|
virtual HRESULT SetFloat (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ double value) = 0;
|
||||||
|
virtual HRESULT GetFloat (/* in */ BMDDeckLinkConfigurationID cfgID, /* out */ double *value) = 0;
|
||||||
|
virtual HRESULT SetString (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ const char *value) = 0;
|
||||||
|
virtual HRESULT GetString (/* in */ BMDDeckLinkConfigurationID cfgID, /* out */ const char **value) = 0;
|
||||||
|
virtual HRESULT WriteConfigurationToPreferences (void) = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual ~IDeckLinkConfiguration_v10_2 () {} // call Release method to drop reference count
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /* defined(BMD_DECKLINKAPICONFIGURATION_v10_2_H) */
|
||||||
76
services/capture/sdk/DeckLinkAPIConfiguration_v10_4.h
Normal file
76
services/capture/sdk/DeckLinkAPIConfiguration_v10_4.h
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
/* -LICENSE-START-
|
||||||
|
** Copyright (c) 2015 Blackmagic Design
|
||||||
|
**
|
||||||
|
** Permission is hereby granted, free of charge, to any person or organization
|
||||||
|
** obtaining a copy of the software and accompanying documentation (the
|
||||||
|
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||||
|
** and transmit the Software, and to prepare derivative works of the Software,
|
||||||
|
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||||
|
** accordance with:
|
||||||
|
**
|
||||||
|
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||||
|
** Agreement for the Software Development Kit ("EULA") available at
|
||||||
|
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||||
|
**
|
||||||
|
** (2) if the Software is obtained from any third party, such licensing terms
|
||||||
|
** as notified by that third party,
|
||||||
|
**
|
||||||
|
** and all subject to the following:
|
||||||
|
**
|
||||||
|
** (3) the copyright notices in the Software and this entire statement,
|
||||||
|
** including the above license grant, this restriction and the following
|
||||||
|
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||||
|
** part, and all derivative works of the Software, unless such copies or
|
||||||
|
** derivative works are solely in the form of machine-executable object code
|
||||||
|
** generated by a source language processor.
|
||||||
|
**
|
||||||
|
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||||
|
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||||
|
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||||
|
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
** DEALINGS IN THE SOFTWARE.
|
||||||
|
**
|
||||||
|
** A copy of the Software is available free of charge at
|
||||||
|
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||||
|
**
|
||||||
|
** -LICENSE-END-
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef BMD_DECKLINKAPICONFIGURATION_v10_4_H
|
||||||
|
#define BMD_DECKLINKAPICONFIGURATION_v10_4_H
|
||||||
|
|
||||||
|
#include "DeckLinkAPIConfiguration.h"
|
||||||
|
|
||||||
|
// Interface ID Declarations
|
||||||
|
|
||||||
|
BMD_CONST REFIID IID_IDeckLinkConfiguration_v10_4 = /* 1E69FCF6-4203-4936-8076-2A9F4CFD50CB */ {0x1E,0x69,0xFC,0xF6,0x42,0x03,0x49,0x36,0x80,0x76,0x2A,0x9F,0x4C,0xFD,0x50,0xCB};
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
// Forward Declarations
|
||||||
|
|
||||||
|
class IDeckLinkConfiguration_v10_4;
|
||||||
|
|
||||||
|
/* Interface IDeckLinkConfiguration_v10_4 - DeckLink Configuration interface */
|
||||||
|
|
||||||
|
class BMD_PUBLIC IDeckLinkConfiguration_v10_4 : public IUnknown
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual HRESULT SetFlag (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ bool value) = 0;
|
||||||
|
virtual HRESULT GetFlag (/* in */ BMDDeckLinkConfigurationID cfgID, /* out */ bool *value) = 0;
|
||||||
|
virtual HRESULT SetInt (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ int64_t value) = 0;
|
||||||
|
virtual HRESULT GetInt (/* in */ BMDDeckLinkConfigurationID cfgID, /* out */ int64_t *value) = 0;
|
||||||
|
virtual HRESULT SetFloat (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ double value) = 0;
|
||||||
|
virtual HRESULT GetFloat (/* in */ BMDDeckLinkConfigurationID cfgID, /* out */ double *value) = 0;
|
||||||
|
virtual HRESULT SetString (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ const char *value) = 0;
|
||||||
|
virtual HRESULT GetString (/* in */ BMDDeckLinkConfigurationID cfgID, /* out */ const char **value) = 0;
|
||||||
|
virtual HRESULT WriteConfigurationToPreferences (void) = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual ~IDeckLinkConfiguration_v10_4 () {} // call Release method to drop reference count
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
#endif /* defined(BMD_DECKLINKAPICONFIGURATION_v10_4_H) */
|
||||||
73
services/capture/sdk/DeckLinkAPIConfiguration_v10_5.h
Normal file
73
services/capture/sdk/DeckLinkAPIConfiguration_v10_5.h
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
/* -LICENSE-START-
|
||||||
|
** Copyright (c) 2015 Blackmagic Design
|
||||||
|
**
|
||||||
|
** Permission is hereby granted, free of charge, to any person or organization
|
||||||
|
** obtaining a copy of the software and accompanying documentation (the
|
||||||
|
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||||
|
** and transmit the Software, and to prepare derivative works of the Software,
|
||||||
|
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||||
|
** accordance with:
|
||||||
|
**
|
||||||
|
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||||
|
** Agreement for the Software Development Kit ("EULA") available at
|
||||||
|
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||||
|
**
|
||||||
|
** (2) if the Software is obtained from any third party, such licensing terms
|
||||||
|
** as notified by that third party,
|
||||||
|
**
|
||||||
|
** and all subject to the following:
|
||||||
|
**
|
||||||
|
** (3) the copyright notices in the Software and this entire statement,
|
||||||
|
** including the above license grant, this restriction and the following
|
||||||
|
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||||
|
** part, and all derivative works of the Software, unless such copies or
|
||||||
|
** derivative works are solely in the form of machine-executable object code
|
||||||
|
** generated by a source language processor.
|
||||||
|
**
|
||||||
|
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||||
|
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||||
|
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||||
|
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
** DEALINGS IN THE SOFTWARE.
|
||||||
|
**
|
||||||
|
** A copy of the Software is available free of charge at
|
||||||
|
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||||
|
**
|
||||||
|
** -LICENSE-END-
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef BMD_DECKLINKAPICONFIGURATION_v10_5_H
|
||||||
|
#define BMD_DECKLINKAPICONFIGURATION_v10_5_H
|
||||||
|
|
||||||
|
#include "DeckLinkAPIConfiguration.h"
|
||||||
|
|
||||||
|
// Interface ID Declarations
|
||||||
|
|
||||||
|
BMD_CONST REFIID IID_IDeckLinkEncoderConfiguration_v10_5 = /* 67455668-0848-45DF-8D8E-350A77C9A028 */ {0x67,0x45,0x56,0x68,0x08,0x48,0x45,0xDF,0x8D,0x8E,0x35,0x0A,0x77,0xC9,0xA0,0x28};
|
||||||
|
|
||||||
|
// Forward Declarations
|
||||||
|
|
||||||
|
class IDeckLinkEncoderConfiguration_v10_5;
|
||||||
|
|
||||||
|
/* Interface IDeckLinkEncoderConfiguration_v10_5 - DeckLink Encoder Configuration interface. Obtained from IDeckLinkEncoderInput */
|
||||||
|
|
||||||
|
class BMD_PUBLIC IDeckLinkEncoderConfiguration_v10_5 : public IUnknown
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual HRESULT SetFlag (/* in */ BMDDeckLinkEncoderConfigurationID cfgID, /* in */ bool value) = 0;
|
||||||
|
virtual HRESULT GetFlag (/* in */ BMDDeckLinkEncoderConfigurationID cfgID, /* out */ bool *value) = 0;
|
||||||
|
virtual HRESULT SetInt (/* in */ BMDDeckLinkEncoderConfigurationID cfgID, /* in */ int64_t value) = 0;
|
||||||
|
virtual HRESULT GetInt (/* in */ BMDDeckLinkEncoderConfigurationID cfgID, /* out */ int64_t *value) = 0;
|
||||||
|
virtual HRESULT SetFloat (/* in */ BMDDeckLinkEncoderConfigurationID cfgID, /* in */ double value) = 0;
|
||||||
|
virtual HRESULT GetFloat (/* in */ BMDDeckLinkEncoderConfigurationID cfgID, /* out */ double *value) = 0;
|
||||||
|
virtual HRESULT SetString (/* in */ BMDDeckLinkEncoderConfigurationID cfgID, /* in */ const char *value) = 0;
|
||||||
|
virtual HRESULT GetString (/* in */ BMDDeckLinkEncoderConfigurationID cfgID, /* out */ const char **value) = 0;
|
||||||
|
virtual HRESULT GetDecoderConfigurationInfo (/* out */ void *buffer, /* in */ long bufferSize, /* out */ long *returnedSize) = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual ~IDeckLinkEncoderConfiguration_v10_5 () {} // call Release method to drop reference count
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /* defined(BMD_DECKLINKAPICONFIGURATION_v10_5_H) */
|
||||||
75
services/capture/sdk/DeckLinkAPIConfiguration_v10_9.h
Normal file
75
services/capture/sdk/DeckLinkAPIConfiguration_v10_9.h
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
/* -LICENSE-START-
|
||||||
|
** Copyright (c) 2017 Blackmagic Design
|
||||||
|
**
|
||||||
|
** Permission is hereby granted, free of charge, to any person or organization
|
||||||
|
** obtaining a copy of the software and accompanying documentation (the
|
||||||
|
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||||
|
** and transmit the Software, and to prepare derivative works of the Software,
|
||||||
|
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||||
|
** accordance with:
|
||||||
|
**
|
||||||
|
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||||
|
** Agreement for the Software Development Kit ("EULA") available at
|
||||||
|
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||||
|
**
|
||||||
|
** (2) if the Software is obtained from any third party, such licensing terms
|
||||||
|
** as notified by that third party,
|
||||||
|
**
|
||||||
|
** and all subject to the following:
|
||||||
|
**
|
||||||
|
** (3) the copyright notices in the Software and this entire statement,
|
||||||
|
** including the above license grant, this restriction and the following
|
||||||
|
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||||
|
** part, and all derivative works of the Software, unless such copies or
|
||||||
|
** derivative works are solely in the form of machine-executable object code
|
||||||
|
** generated by a source language processor.
|
||||||
|
**
|
||||||
|
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||||
|
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||||
|
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||||
|
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
** DEALINGS IN THE SOFTWARE.
|
||||||
|
**
|
||||||
|
** A copy of the Software is available free of charge at
|
||||||
|
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||||
|
**
|
||||||
|
** -LICENSE-END-
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef BMD_DECKLINKAPICONFIGURATION_v10_9_H
|
||||||
|
#define BMD_DECKLINKAPICONFIGURATION_v10_9_H
|
||||||
|
|
||||||
|
#include "DeckLinkAPIConfiguration.h"
|
||||||
|
|
||||||
|
// Interface ID Declarations
|
||||||
|
|
||||||
|
BMD_CONST REFIID IID_IDeckLinkConfiguration_v10_9 = /* CB71734A-FE37-4E8D-8E13-802133A1C3F2 */ {0xCB,0x71,0x73,0x4A,0xFE,0x37,0x4E,0x8D,0x8E,0x13,0x80,0x21,0x33,0xA1,0xC3,0xF2};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Forward Declarations
|
||||||
|
|
||||||
|
class IDeckLinkConfiguration_v10_9;
|
||||||
|
|
||||||
|
/* Interface IDeckLinkConfiguration_v10_9 - DeckLink Configuration interface */
|
||||||
|
|
||||||
|
class BMD_PUBLIC IDeckLinkConfiguration_v10_9 : public IUnknown
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual HRESULT SetFlag (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ bool value) = 0;
|
||||||
|
virtual HRESULT GetFlag (/* in */ BMDDeckLinkConfigurationID cfgID, /* out */ bool *value) = 0;
|
||||||
|
virtual HRESULT SetInt (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ int64_t value) = 0;
|
||||||
|
virtual HRESULT GetInt (/* in */ BMDDeckLinkConfigurationID cfgID, /* out */ int64_t *value) = 0;
|
||||||
|
virtual HRESULT SetFloat (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ double value) = 0;
|
||||||
|
virtual HRESULT GetFloat (/* in */ BMDDeckLinkConfigurationID cfgID, /* out */ double *value) = 0;
|
||||||
|
virtual HRESULT SetString (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ const char *value) = 0;
|
||||||
|
virtual HRESULT GetString (/* in */ BMDDeckLinkConfigurationID cfgID, /* out */ const char **value) = 0;
|
||||||
|
virtual HRESULT WriteConfigurationToPreferences (void) = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual ~IDeckLinkConfiguration_v10_9 () {} // call Release method to drop reference count
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
#endif /* defined(BMD_DECKLINKAPICONFIGURATION_v10_9_H) */
|
||||||
84
services/capture/sdk/DeckLinkAPIConfiguration_v15_3_1.h
Normal file
84
services/capture/sdk/DeckLinkAPIConfiguration_v15_3_1.h
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
/* -LICENSE-START-
|
||||||
|
** Copyright (c) 2026 Blackmagic Design
|
||||||
|
**
|
||||||
|
** Permission is hereby granted, free of charge, to any person or organization
|
||||||
|
** obtaining a copy of the software and accompanying documentation (the
|
||||||
|
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||||
|
** and transmit the Software, and to prepare derivative works of the Software,
|
||||||
|
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||||
|
** accordance with:
|
||||||
|
**
|
||||||
|
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||||
|
** Agreement for the Software Development Kit ("EULA") available at
|
||||||
|
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||||
|
**
|
||||||
|
** (2) if the Software is obtained from any third party, such licensing terms
|
||||||
|
** as notified by that third party,
|
||||||
|
**
|
||||||
|
** and all subject to the following:
|
||||||
|
**
|
||||||
|
** (3) the copyright notices in the Software and this entire statement,
|
||||||
|
** including the above license grant, this restriction and the following
|
||||||
|
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||||
|
** part, and all derivative works of the Software, unless such copies or
|
||||||
|
** derivative works are solely in the form of machine-executable object code
|
||||||
|
** generated by a source language processor.
|
||||||
|
**
|
||||||
|
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||||
|
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||||
|
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||||
|
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
** DEALINGS IN THE SOFTWARE.
|
||||||
|
**
|
||||||
|
** A copy of the Software is available free of charge at
|
||||||
|
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||||
|
**
|
||||||
|
** -LICENSE-END-
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "DeckLinkAPIConfiguration.h"
|
||||||
|
|
||||||
|
/* Enum BMDDeckLinkConfigurationID_v15_3_1 - DeckLink Configuration ID */
|
||||||
|
typedef uint32_t BMDDeckLinkConfigurationID_v15_3_1;
|
||||||
|
enum _BMDDeckLinkConfigurationID_v15_3_1
|
||||||
|
{
|
||||||
|
/* Network Flags */
|
||||||
|
bmdDeckLinkConfigEthernetUseDHCP_v15_3_1 = /* 'DHCP' */ 0x44484350,
|
||||||
|
|
||||||
|
/* Network Strings */
|
||||||
|
bmdDeckLinkConfigEthernetStaticLocalIPAddress_v15_3_1 = /* 'nsip' */ 0x6E736970,
|
||||||
|
bmdDeckLinkConfigEthernetStaticSubnetMask_v15_3_1 = /* 'nssm' */ 0x6E73736D,
|
||||||
|
bmdDeckLinkConfigEthernetStaticGatewayIPAddress_v15_3_1 = /* 'nsgw' */ 0x6E736777,
|
||||||
|
bmdDeckLinkConfigEthernetStaticPrimaryDNS_v15_3_1 = /* 'nspd' */ 0x6E737064,
|
||||||
|
bmdDeckLinkConfigEthernetStaticSecondaryDNS_v15_3_1 = /* 'nssd' */ 0x6E737364,
|
||||||
|
bmdDeckLinkConfigEthernetVideoOutputAddress_v15_3_1 = /* 'noav' */ 0x6E6F6176,
|
||||||
|
bmdDeckLinkConfigEthernetAudioOutputAddress_v15_3_1 = /* 'noaa' */ 0x6E6F6161,
|
||||||
|
bmdDeckLinkConfigEthernetAncillaryOutputAddress_v15_3_1 = /* 'noaA' */ 0x6E6F6141,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Interface ID Declarations
|
||||||
|
|
||||||
|
BMD_CONST REFIID IID_IDeckLinkConfiguration_v15_3_1 = /* 912F634B-2D4E-40A4-8AAB-8D80B73F1289 */ {0x91,0x2F,0x63,0x4B,0x2D,0x4E,0x40,0xA4,0x8A,0xAB,0x8D,0x80,0xB7,0x3F,0x12,0x89};
|
||||||
|
|
||||||
|
/* Interface IDeckLinkConfiguration_v15_3_1 - DeckLink Configuration interface */
|
||||||
|
|
||||||
|
class IDeckLinkConfiguration_v15_3_1 : public IUnknown
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual HRESULT SetFlag (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ bool value) = 0;
|
||||||
|
virtual HRESULT GetFlag (/* in */ BMDDeckLinkConfigurationID cfgID, /* out */ bool *value) = 0;
|
||||||
|
virtual HRESULT SetInt (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ int64_t value) = 0;
|
||||||
|
virtual HRESULT GetInt (/* in */ BMDDeckLinkConfigurationID cfgID, /* out */ int64_t *value) = 0;
|
||||||
|
virtual HRESULT SetFloat (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ double value) = 0;
|
||||||
|
virtual HRESULT GetFloat (/* in */ BMDDeckLinkConfigurationID cfgID, /* out */ double *value) = 0;
|
||||||
|
virtual HRESULT SetString (/* in */ BMDDeckLinkConfigurationID cfgID, /* in */ const char* value) = 0;
|
||||||
|
virtual HRESULT GetString (/* in */ BMDDeckLinkConfigurationID cfgID, /* out */ const char* *value) = 0;
|
||||||
|
virtual HRESULT WriteConfigurationToPreferences (void) = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual ~IDeckLinkConfiguration_v15_3_1 () {} // call Release method to drop reference count
|
||||||
|
};
|
||||||
227
services/capture/sdk/DeckLinkAPIDeckControl.h
Normal file
227
services/capture/sdk/DeckLinkAPIDeckControl.h
Normal file
|
|
@ -0,0 +1,227 @@
|
||||||
|
/* -LICENSE-START-
|
||||||
|
** Copyright (c) 2026 Blackmagic Design
|
||||||
|
**
|
||||||
|
** Permission is hereby granted, free of charge, to any person or organization
|
||||||
|
** obtaining a copy of the software and accompanying documentation covered by
|
||||||
|
** this license (the "Software") to use, reproduce, display, distribute,
|
||||||
|
** execute, and transmit the Software, and to prepare derivative works of the
|
||||||
|
** Software, and to permit third-parties to whom the Software is furnished to
|
||||||
|
** do so, all subject to the following:
|
||||||
|
**
|
||||||
|
** The copyright notices in the Software and this entire statement, including
|
||||||
|
** the above license grant, this restriction and the following disclaimer,
|
||||||
|
** must be included in all copies of the Software, in whole or in part, and
|
||||||
|
** all derivative works of the Software, unless such copies or derivative
|
||||||
|
** works are solely in the form of machine-executable object code generated by
|
||||||
|
** a source language processor.
|
||||||
|
**
|
||||||
|
** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||||
|
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||||
|
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||||
|
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
** DEALINGS IN THE SOFTWARE.
|
||||||
|
** -LICENSE-END-
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -- AUTOMATICALLY GENERATED - DO NOT EDIT ---
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef BMD_DECKLINKAPIDECKCONTROL_H
|
||||||
|
#define BMD_DECKLINKAPIDECKCONTROL_H
|
||||||
|
|
||||||
|
|
||||||
|
#ifndef BMD_CONST
|
||||||
|
#if defined(_MSC_VER)
|
||||||
|
#define BMD_CONST __declspec(selectany) static const
|
||||||
|
#else
|
||||||
|
#define BMD_CONST static const
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef BMD_PUBLIC
|
||||||
|
#define BMD_PUBLIC
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Type Declarations
|
||||||
|
|
||||||
|
|
||||||
|
// Interface ID Declarations
|
||||||
|
|
||||||
|
BMD_CONST REFIID IID_IDeckLinkDeckControlStatusCallback = /* 53436FFB-B434-4906-BADC-AE3060FFE8EF */ { 0x53,0x43,0x6F,0xFB,0xB4,0x34,0x49,0x06,0xBA,0xDC,0xAE,0x30,0x60,0xFF,0xE8,0xEF };
|
||||||
|
BMD_CONST REFIID IID_IDeckLinkDeckControl = /* 8E1C3ACE-19C7-4E00-8B92-D80431D958BE */ { 0x8E,0x1C,0x3A,0xCE,0x19,0xC7,0x4E,0x00,0x8B,0x92,0xD8,0x04,0x31,0xD9,0x58,0xBE };
|
||||||
|
|
||||||
|
/* Enum BMDDeckControlMode - DeckControl mode */
|
||||||
|
|
||||||
|
typedef uint32_t BMDDeckControlMode;
|
||||||
|
enum _BMDDeckControlMode {
|
||||||
|
bmdDeckControlNotOpened = /* 'ntop' */ 0x6E746F70,
|
||||||
|
bmdDeckControlVTRControlMode = /* 'vtrc' */ 0x76747263,
|
||||||
|
bmdDeckControlExportMode = /* 'expm' */ 0x6578706D,
|
||||||
|
bmdDeckControlCaptureMode = /* 'capm' */ 0x6361706D
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Enum BMDDeckControlEvent - DeckControl event */
|
||||||
|
|
||||||
|
typedef uint32_t BMDDeckControlEvent;
|
||||||
|
enum _BMDDeckControlEvent {
|
||||||
|
bmdDeckControlAbortedEvent = /* 'abte' */ 0x61627465, // This event is triggered when a capture or edit-to-tape operation is aborted.
|
||||||
|
|
||||||
|
/* Export-To-Tape events */
|
||||||
|
|
||||||
|
bmdDeckControlPrepareForExportEvent = /* 'pfee' */ 0x70666565, // This event is triggered a few frames before reaching the in-point. IDeckLinkInput::StartScheduledPlayback should be called at this point.
|
||||||
|
bmdDeckControlExportCompleteEvent = /* 'exce' */ 0x65786365, // This event is triggered a few frames after reaching the out-point. At this point, it is safe to stop playback. Upon reception of this event the deck's control mode is set back to bmdDeckControlVTRControlMode.
|
||||||
|
|
||||||
|
/* Capture events */
|
||||||
|
|
||||||
|
bmdDeckControlPrepareForCaptureEvent = /* 'pfce' */ 0x70666365, // This event is triggered a few frames before reaching the in-point. The serial timecode attached to IDeckLinkVideoInputFrames is now valid.
|
||||||
|
bmdDeckControlCaptureCompleteEvent = /* 'ccev' */ 0x63636576 // This event is triggered a few frames after reaching the out-point. Upon reception of this event the deck's control mode is set back to bmdDeckControlVTRControlMode.
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Enum BMDDeckControlVTRControlState - VTR Control state */
|
||||||
|
|
||||||
|
typedef uint32_t BMDDeckControlVTRControlState;
|
||||||
|
enum _BMDDeckControlVTRControlState {
|
||||||
|
bmdDeckControlNotInVTRControlMode = /* 'nvcm' */ 0x6E76636D,
|
||||||
|
bmdDeckControlVTRControlPlaying = /* 'vtrp' */ 0x76747270,
|
||||||
|
bmdDeckControlVTRControlRecording = /* 'vtrr' */ 0x76747272,
|
||||||
|
bmdDeckControlVTRControlStill = /* 'vtra' */ 0x76747261,
|
||||||
|
bmdDeckControlVTRControlShuttleForward = /* 'vtsf' */ 0x76747366,
|
||||||
|
bmdDeckControlVTRControlShuttleReverse = /* 'vtsr' */ 0x76747372,
|
||||||
|
bmdDeckControlVTRControlJogForward = /* 'vtjf' */ 0x76746A66,
|
||||||
|
bmdDeckControlVTRControlJogReverse = /* 'vtjr' */ 0x76746A72,
|
||||||
|
bmdDeckControlVTRControlStopped = /* 'vtro' */ 0x7674726F
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Enum BMDDeckControlStatusFlags - Deck Control status flags */
|
||||||
|
|
||||||
|
typedef uint32_t BMDDeckControlStatusFlags;
|
||||||
|
enum _BMDDeckControlStatusFlags {
|
||||||
|
bmdDeckControlStatusDeckConnected = 1 << 0,
|
||||||
|
bmdDeckControlStatusRemoteMode = 1 << 1,
|
||||||
|
bmdDeckControlStatusRecordInhibited = 1 << 2,
|
||||||
|
bmdDeckControlStatusCassetteOut = 1 << 3
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Enum BMDDeckControlExportModeOpsFlags - Export mode flags */
|
||||||
|
|
||||||
|
typedef uint32_t BMDDeckControlExportModeOpsFlags;
|
||||||
|
enum _BMDDeckControlExportModeOpsFlags {
|
||||||
|
bmdDeckControlExportModeInsertVideo = 1 << 0,
|
||||||
|
bmdDeckControlExportModeInsertAudio1 = 1 << 1,
|
||||||
|
bmdDeckControlExportModeInsertAudio2 = 1 << 2,
|
||||||
|
bmdDeckControlExportModeInsertAudio3 = 1 << 3,
|
||||||
|
bmdDeckControlExportModeInsertAudio4 = 1 << 4,
|
||||||
|
bmdDeckControlExportModeInsertAudio5 = 1 << 5,
|
||||||
|
bmdDeckControlExportModeInsertAudio6 = 1 << 6,
|
||||||
|
bmdDeckControlExportModeInsertAudio7 = 1 << 7,
|
||||||
|
bmdDeckControlExportModeInsertAudio8 = 1 << 8,
|
||||||
|
bmdDeckControlExportModeInsertAudio9 = 1 << 9,
|
||||||
|
bmdDeckControlExportModeInsertAudio10 = 1 << 10,
|
||||||
|
bmdDeckControlExportModeInsertAudio11 = 1 << 11,
|
||||||
|
bmdDeckControlExportModeInsertAudio12 = 1 << 12,
|
||||||
|
bmdDeckControlExportModeInsertTimeCode = 1 << 13,
|
||||||
|
bmdDeckControlExportModeInsertAssemble = 1 << 14,
|
||||||
|
bmdDeckControlExportModeInsertPreview = 1 << 15,
|
||||||
|
bmdDeckControlUseManualExport = 1 << 16
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Enum BMDDeckControlError - Deck Control error */
|
||||||
|
|
||||||
|
typedef uint32_t BMDDeckControlError;
|
||||||
|
enum _BMDDeckControlError {
|
||||||
|
bmdDeckControlNoError = /* 'noer' */ 0x6E6F6572,
|
||||||
|
bmdDeckControlModeError = /* 'moer' */ 0x6D6F6572,
|
||||||
|
bmdDeckControlMissedInPointError = /* 'mier' */ 0x6D696572,
|
||||||
|
bmdDeckControlDeckTimeoutError = /* 'dter' */ 0x64746572,
|
||||||
|
bmdDeckControlCommandFailedError = /* 'cfer' */ 0x63666572,
|
||||||
|
bmdDeckControlDeviceAlreadyOpenedError = /* 'dalo' */ 0x64616C6F,
|
||||||
|
bmdDeckControlFailedToOpenDeviceError = /* 'fder' */ 0x66646572,
|
||||||
|
bmdDeckControlInLocalModeError = /* 'lmer' */ 0x6C6D6572,
|
||||||
|
bmdDeckControlEndOfTapeError = /* 'eter' */ 0x65746572,
|
||||||
|
bmdDeckControlUserAbortError = /* 'uaer' */ 0x75616572,
|
||||||
|
bmdDeckControlNoTapeInDeckError = /* 'nter' */ 0x6E746572,
|
||||||
|
bmdDeckControlNoVideoFromCardError = /* 'nvfc' */ 0x6E766663,
|
||||||
|
bmdDeckControlNoCommunicationError = /* 'ncom' */ 0x6E636F6D,
|
||||||
|
bmdDeckControlBufferTooSmallError = /* 'btsm' */ 0x6274736D,
|
||||||
|
bmdDeckControlBadChecksumError = /* 'chks' */ 0x63686B73,
|
||||||
|
bmdDeckControlUnknownError = /* 'uner' */ 0x756E6572
|
||||||
|
};
|
||||||
|
|
||||||
|
#if defined(__cplusplus)
|
||||||
|
|
||||||
|
// Forward Declarations
|
||||||
|
|
||||||
|
class IDeckLinkDeckControlStatusCallback;
|
||||||
|
class IDeckLinkDeckControl;
|
||||||
|
|
||||||
|
/* Interface IDeckLinkDeckControlStatusCallback - Deck control state change callback. */
|
||||||
|
|
||||||
|
class BMD_PUBLIC IDeckLinkDeckControlStatusCallback : public IUnknown
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual HRESULT TimecodeUpdate (/* in */ BMDTimecodeBCD currentTimecode) = 0;
|
||||||
|
virtual HRESULT VTRControlStateChanged (/* in */ BMDDeckControlVTRControlState newState, /* in */ BMDDeckControlError error) = 0;
|
||||||
|
virtual HRESULT DeckControlEventReceived (/* in */ BMDDeckControlEvent event, /* in */ BMDDeckControlError error) = 0;
|
||||||
|
virtual HRESULT DeckControlStatusChanged (/* in */ BMDDeckControlStatusFlags flags, /* in */ uint32_t mask) = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual ~IDeckLinkDeckControlStatusCallback () {} // call Release method to drop reference count
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Interface IDeckLinkDeckControl - Deck Control main interface */
|
||||||
|
|
||||||
|
class BMD_PUBLIC IDeckLinkDeckControl : public IUnknown
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual HRESULT Open (/* in */ BMDTimeScale timeScale, /* in */ BMDTimeValue timeValue, /* in */ bool timecodeIsDropFrame, /* out */ BMDDeckControlError* error) = 0;
|
||||||
|
virtual HRESULT Close (/* in */ bool standbyOn) = 0;
|
||||||
|
virtual HRESULT GetCurrentState (/* out */ BMDDeckControlMode* mode, /* out */ BMDDeckControlVTRControlState* vtrControlState, /* out */ BMDDeckControlStatusFlags* flags) = 0;
|
||||||
|
virtual HRESULT SetStandby (/* in */ bool standbyOn) = 0;
|
||||||
|
virtual HRESULT SendCommand (/* in */ uint8_t* inBuffer, /* in */ uint32_t inBufferSize, /* out */ uint8_t* outBuffer, /* out */ uint32_t* outDataSize, /* in */ uint32_t outBufferSize, /* out */ BMDDeckControlError* error) = 0;
|
||||||
|
virtual HRESULT Play (/* out */ BMDDeckControlError* error) = 0;
|
||||||
|
virtual HRESULT Stop (/* out */ BMDDeckControlError* error) = 0;
|
||||||
|
virtual HRESULT TogglePlayStop (/* out */ BMDDeckControlError* error) = 0;
|
||||||
|
virtual HRESULT Eject (/* out */ BMDDeckControlError* error) = 0;
|
||||||
|
virtual HRESULT GoToTimecode (/* in */ BMDTimecodeBCD timecode, /* out */ BMDDeckControlError* error) = 0;
|
||||||
|
virtual HRESULT FastForward (/* in */ bool viewTape, /* out */ BMDDeckControlError* error) = 0;
|
||||||
|
virtual HRESULT Rewind (/* in */ bool viewTape, /* out */ BMDDeckControlError* error) = 0;
|
||||||
|
virtual HRESULT StepForward (/* out */ BMDDeckControlError* error) = 0;
|
||||||
|
virtual HRESULT StepBack (/* out */ BMDDeckControlError* error) = 0;
|
||||||
|
virtual HRESULT Jog (/* in */ double rate, /* out */ BMDDeckControlError* error) = 0;
|
||||||
|
virtual HRESULT Shuttle (/* in */ double rate, /* out */ BMDDeckControlError* error) = 0;
|
||||||
|
virtual HRESULT GetTimecodeString (/* out */ const char** currentTimeCode, /* out */ BMDDeckControlError* error) = 0;
|
||||||
|
virtual HRESULT GetTimecode (/* out */ IDeckLinkTimecode** currentTimecode, /* out */ BMDDeckControlError* error) = 0;
|
||||||
|
virtual HRESULT GetTimecodeBCD (/* out */ BMDTimecodeBCD* currentTimecode, /* out */ BMDDeckControlError* error) = 0;
|
||||||
|
virtual HRESULT SetPreroll (/* in */ uint32_t prerollSeconds) = 0;
|
||||||
|
virtual HRESULT GetPreroll (/* out */ uint32_t* prerollSeconds) = 0;
|
||||||
|
virtual HRESULT SetExportOffset (/* in */ int32_t exportOffsetFields) = 0;
|
||||||
|
virtual HRESULT GetExportOffset (/* out */ int32_t* exportOffsetFields) = 0;
|
||||||
|
virtual HRESULT GetManualExportOffset (/* out */ int32_t* deckManualExportOffsetFields) = 0;
|
||||||
|
virtual HRESULT SetCaptureOffset (/* in */ int32_t captureOffsetFields) = 0;
|
||||||
|
virtual HRESULT GetCaptureOffset (/* out */ int32_t* captureOffsetFields) = 0;
|
||||||
|
virtual HRESULT StartExport (/* in */ BMDTimecodeBCD inTimecode, /* in */ BMDTimecodeBCD outTimecode, /* in */ BMDDeckControlExportModeOpsFlags exportModeOps, /* out */ BMDDeckControlError* error) = 0;
|
||||||
|
virtual HRESULT StartCapture (/* in */ bool useVITC, /* in */ BMDTimecodeBCD inTimecode, /* in */ BMDTimecodeBCD outTimecode, /* out */ BMDDeckControlError* error) = 0;
|
||||||
|
virtual HRESULT GetDeviceID (/* out */ uint16_t* deviceId, /* out */ BMDDeckControlError* error) = 0;
|
||||||
|
virtual HRESULT Abort (void) = 0;
|
||||||
|
virtual HRESULT CrashRecordStart (/* out */ BMDDeckControlError* error) = 0;
|
||||||
|
virtual HRESULT CrashRecordStop (/* out */ BMDDeckControlError* error) = 0;
|
||||||
|
virtual HRESULT SetCallback (/* in */ IDeckLinkDeckControlStatusCallback* callback) = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual ~IDeckLinkDeckControl () {} // call Release method to drop reference count
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Functions */
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#endif /* defined(__cplusplus) */
|
||||||
|
#endif /* defined(BMD_DECKLINKAPIDECKCONTROL_H) */
|
||||||
83
services/capture/sdk/DeckLinkAPIDiscovery.h
Normal file
83
services/capture/sdk/DeckLinkAPIDiscovery.h
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
/* -LICENSE-START-
|
||||||
|
** Copyright (c) 2026 Blackmagic Design
|
||||||
|
**
|
||||||
|
** Permission is hereby granted, free of charge, to any person or organization
|
||||||
|
** obtaining a copy of the software and accompanying documentation covered by
|
||||||
|
** this license (the "Software") to use, reproduce, display, distribute,
|
||||||
|
** execute, and transmit the Software, and to prepare derivative works of the
|
||||||
|
** Software, and to permit third-parties to whom the Software is furnished to
|
||||||
|
** do so, all subject to the following:
|
||||||
|
**
|
||||||
|
** The copyright notices in the Software and this entire statement, including
|
||||||
|
** the above license grant, this restriction and the following disclaimer,
|
||||||
|
** must be included in all copies of the Software, in whole or in part, and
|
||||||
|
** all derivative works of the Software, unless such copies or derivative
|
||||||
|
** works are solely in the form of machine-executable object code generated by
|
||||||
|
** a source language processor.
|
||||||
|
**
|
||||||
|
** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||||
|
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||||
|
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||||
|
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
** DEALINGS IN THE SOFTWARE.
|
||||||
|
** -LICENSE-END-
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -- AUTOMATICALLY GENERATED - DO NOT EDIT ---
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef BMD_DECKLINKAPIDISCOVERY_H
|
||||||
|
#define BMD_DECKLINKAPIDISCOVERY_H
|
||||||
|
|
||||||
|
|
||||||
|
#ifndef BMD_CONST
|
||||||
|
#if defined(_MSC_VER)
|
||||||
|
#define BMD_CONST __declspec(selectany) static const
|
||||||
|
#else
|
||||||
|
#define BMD_CONST static const
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef BMD_PUBLIC
|
||||||
|
#define BMD_PUBLIC
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Type Declarations
|
||||||
|
|
||||||
|
|
||||||
|
// Interface ID Declarations
|
||||||
|
|
||||||
|
BMD_CONST REFIID IID_IDeckLink = /* C418FBDD-0587-48ED-8FE5-640F0A14AF91 */ { 0xC4,0x18,0xFB,0xDD,0x05,0x87,0x48,0xED,0x8F,0xE5,0x64,0x0F,0x0A,0x14,0xAF,0x91 };
|
||||||
|
|
||||||
|
#if defined(__cplusplus)
|
||||||
|
|
||||||
|
// Forward Declarations
|
||||||
|
|
||||||
|
class IDeckLink;
|
||||||
|
|
||||||
|
/* Interface IDeckLink - Represents a DeckLink device */
|
||||||
|
|
||||||
|
class BMD_PUBLIC IDeckLink : public IUnknown
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual HRESULT GetModelName (/* out */ const char** modelName) = 0;
|
||||||
|
virtual HRESULT GetDisplayName (/* out */ const char** displayName) = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual ~IDeckLink () {} // call Release method to drop reference count
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Functions */
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#endif /* defined(__cplusplus) */
|
||||||
|
#endif /* defined(BMD_DECKLINKAPIDISCOVERY_H) */
|
||||||
188
services/capture/sdk/DeckLinkAPIDispatch.cpp
Normal file
188
services/capture/sdk/DeckLinkAPIDispatch.cpp
Normal file
|
|
@ -0,0 +1,188 @@
|
||||||
|
/* -LICENSE-START-
|
||||||
|
** Copyright (c) 2009 Blackmagic Design
|
||||||
|
**
|
||||||
|
** Permission is hereby granted, free of charge, to any person or organization
|
||||||
|
** obtaining a copy of the software and accompanying documentation (the
|
||||||
|
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||||
|
** and transmit the Software, and to prepare derivative works of the Software,
|
||||||
|
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||||
|
** accordance with:
|
||||||
|
**
|
||||||
|
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||||
|
** Agreement for the Software Development Kit ("EULA") available at
|
||||||
|
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||||
|
**
|
||||||
|
** (2) if the Software is obtained from any third party, such licensing terms
|
||||||
|
** as notified by that third party,
|
||||||
|
**
|
||||||
|
** and all subject to the following:
|
||||||
|
**
|
||||||
|
** (3) the copyright notices in the Software and this entire statement,
|
||||||
|
** including the above license grant, this restriction and the following
|
||||||
|
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||||
|
** part, and all derivative works of the Software, unless such copies or
|
||||||
|
** derivative works are solely in the form of machine-executable object code
|
||||||
|
** generated by a source language processor.
|
||||||
|
**
|
||||||
|
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||||
|
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||||
|
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||||
|
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
** DEALINGS IN THE SOFTWARE.
|
||||||
|
**
|
||||||
|
** A copy of the Software is available free of charge at
|
||||||
|
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||||
|
**
|
||||||
|
** -LICENSE-END-
|
||||||
|
**/
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <dlfcn.h>
|
||||||
|
|
||||||
|
#include "DeckLinkAPI.h"
|
||||||
|
|
||||||
|
#define kDeckLinkAPI_Name "libDeckLinkAPI.so"
|
||||||
|
#define KDeckLinkPreviewAPI_Name "libDeckLinkPreviewAPI.so"
|
||||||
|
|
||||||
|
typedef IDeckLinkIterator* (*CreateIteratorFunc)(void);
|
||||||
|
typedef IDeckLinkAPIInformation* (*CreateAPIInformationFunc)(void);
|
||||||
|
typedef IDeckLinkGLScreenPreviewHelper* (*CreateOpenGLScreenPreviewHelperFunc)(void);
|
||||||
|
typedef IDeckLinkGLScreenPreviewHelper* (*CreateOpenGL3ScreenPreviewHelperFunc)(void);
|
||||||
|
typedef IDeckLinkVideoConversion* (*CreateVideoConversionInstanceFunc)(void);
|
||||||
|
typedef IDeckLinkDiscovery* (*CreateDeckLinkDiscoveryInstanceFunc)(void);
|
||||||
|
typedef IDeckLinkVideoFrameAncillaryPackets* (*CreateVideoFrameAncillaryPacketsInstanceFunc)(void);
|
||||||
|
|
||||||
|
static pthread_once_t gDeckLinkOnceControl = PTHREAD_ONCE_INIT;
|
||||||
|
static pthread_once_t gPreviewOnceControl = PTHREAD_ONCE_INIT;
|
||||||
|
|
||||||
|
static bool gLoadedDeckLinkAPI = false;
|
||||||
|
|
||||||
|
static CreateIteratorFunc gCreateIteratorFunc = NULL;
|
||||||
|
static CreateAPIInformationFunc gCreateAPIInformationFunc = NULL;
|
||||||
|
static CreateOpenGLScreenPreviewHelperFunc gCreateOpenGLPreviewFunc = NULL;
|
||||||
|
static CreateOpenGL3ScreenPreviewHelperFunc gCreateOpenGL3PreviewFunc = NULL;
|
||||||
|
static CreateVideoConversionInstanceFunc gCreateVideoConversionFunc = NULL;
|
||||||
|
static CreateDeckLinkDiscoveryInstanceFunc gCreateDeckLinkDiscoveryFunc = NULL;
|
||||||
|
static CreateVideoFrameAncillaryPacketsInstanceFunc gCreateVideoFrameAncillaryPacketsFunc = NULL;
|
||||||
|
|
||||||
|
static void InitDeckLinkAPI (void)
|
||||||
|
{
|
||||||
|
void *libraryHandle;
|
||||||
|
|
||||||
|
libraryHandle = dlopen(kDeckLinkAPI_Name, RTLD_NOW|RTLD_GLOBAL);
|
||||||
|
if (!libraryHandle)
|
||||||
|
{
|
||||||
|
fprintf(stderr, "%s\n", dlerror());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
gLoadedDeckLinkAPI = true;
|
||||||
|
|
||||||
|
gCreateIteratorFunc = (CreateIteratorFunc)dlsym(libraryHandle, "CreateDeckLinkIteratorInstance_0004");
|
||||||
|
if (!gCreateIteratorFunc)
|
||||||
|
fprintf(stderr, "%s\n", dlerror());
|
||||||
|
gCreateAPIInformationFunc = (CreateAPIInformationFunc)dlsym(libraryHandle, "CreateDeckLinkAPIInformationInstance_0001");
|
||||||
|
if (!gCreateAPIInformationFunc)
|
||||||
|
fprintf(stderr, "%s\n", dlerror());
|
||||||
|
gCreateVideoConversionFunc = (CreateVideoConversionInstanceFunc)dlsym(libraryHandle, "CreateVideoConversionInstance_0003");
|
||||||
|
if (!gCreateVideoConversionFunc)
|
||||||
|
fprintf(stderr, "%s\n", dlerror());
|
||||||
|
gCreateDeckLinkDiscoveryFunc = (CreateDeckLinkDiscoveryInstanceFunc)dlsym(libraryHandle, "CreateDeckLinkDiscoveryInstance_0003");
|
||||||
|
if (!gCreateDeckLinkDiscoveryFunc)
|
||||||
|
fprintf(stderr, "%s\n", dlerror());
|
||||||
|
gCreateVideoFrameAncillaryPacketsFunc = (CreateVideoFrameAncillaryPacketsInstanceFunc)dlsym(libraryHandle, "CreateVideoFrameAncillaryPacketsInstance_0002");
|
||||||
|
if (!gCreateVideoFrameAncillaryPacketsFunc)
|
||||||
|
fprintf(stderr, "%s\n", dlerror());
|
||||||
|
}
|
||||||
|
|
||||||
|
static void InitDeckLinkPreviewAPI (void)
|
||||||
|
{
|
||||||
|
void *libraryHandle;
|
||||||
|
|
||||||
|
libraryHandle = dlopen(KDeckLinkPreviewAPI_Name, RTLD_NOW|RTLD_GLOBAL);
|
||||||
|
if (!libraryHandle)
|
||||||
|
{
|
||||||
|
fprintf(stderr, "%s\n", dlerror());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
gCreateOpenGLPreviewFunc = (CreateOpenGLScreenPreviewHelperFunc)dlsym(libraryHandle, "CreateOpenGLScreenPreviewHelper_0002");
|
||||||
|
if (!gCreateOpenGLPreviewFunc)
|
||||||
|
fprintf(stderr, "%s\n", dlerror());
|
||||||
|
gCreateOpenGL3PreviewFunc = (CreateOpenGL3ScreenPreviewHelperFunc)dlsym(libraryHandle, "CreateOpenGL3ScreenPreviewHelper_0002");
|
||||||
|
if (!gCreateOpenGL3PreviewFunc)
|
||||||
|
fprintf(stderr, "%s\n", dlerror());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsDeckLinkAPIPresent (void)
|
||||||
|
{
|
||||||
|
// If the DeckLink API dynamic library was successfully loaded, return this knowledge to the caller
|
||||||
|
return gLoadedDeckLinkAPI;
|
||||||
|
}
|
||||||
|
|
||||||
|
IDeckLinkIterator* CreateDeckLinkIteratorInstance (void)
|
||||||
|
{
|
||||||
|
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
|
||||||
|
|
||||||
|
if (gCreateIteratorFunc == NULL)
|
||||||
|
return NULL;
|
||||||
|
return gCreateIteratorFunc();
|
||||||
|
}
|
||||||
|
|
||||||
|
IDeckLinkAPIInformation* CreateDeckLinkAPIInformationInstance (void)
|
||||||
|
{
|
||||||
|
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
|
||||||
|
|
||||||
|
if (gCreateAPIInformationFunc == NULL)
|
||||||
|
return NULL;
|
||||||
|
return gCreateAPIInformationFunc();
|
||||||
|
}
|
||||||
|
|
||||||
|
IDeckLinkGLScreenPreviewHelper* CreateOpenGLScreenPreviewHelper (void)
|
||||||
|
{
|
||||||
|
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
|
||||||
|
pthread_once(&gPreviewOnceControl, InitDeckLinkPreviewAPI);
|
||||||
|
|
||||||
|
if (gCreateOpenGLPreviewFunc == NULL)
|
||||||
|
return NULL;
|
||||||
|
return gCreateOpenGLPreviewFunc();
|
||||||
|
}
|
||||||
|
|
||||||
|
IDeckLinkGLScreenPreviewHelper* CreateOpenGL3ScreenPreviewHelper (void)
|
||||||
|
{
|
||||||
|
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
|
||||||
|
pthread_once(&gPreviewOnceControl, InitDeckLinkPreviewAPI);
|
||||||
|
|
||||||
|
if (gCreateOpenGL3PreviewFunc == NULL)
|
||||||
|
return NULL;
|
||||||
|
return gCreateOpenGL3PreviewFunc();
|
||||||
|
}
|
||||||
|
|
||||||
|
IDeckLinkVideoConversion* CreateVideoConversionInstance (void)
|
||||||
|
{
|
||||||
|
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
|
||||||
|
|
||||||
|
if (gCreateVideoConversionFunc == NULL)
|
||||||
|
return NULL;
|
||||||
|
return gCreateVideoConversionFunc();
|
||||||
|
}
|
||||||
|
|
||||||
|
IDeckLinkDiscovery* CreateDeckLinkDiscoveryInstance (void)
|
||||||
|
{
|
||||||
|
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
|
||||||
|
|
||||||
|
if (gCreateDeckLinkDiscoveryFunc == NULL)
|
||||||
|
return NULL;
|
||||||
|
return gCreateDeckLinkDiscoveryFunc();
|
||||||
|
}
|
||||||
|
|
||||||
|
IDeckLinkVideoFrameAncillaryPackets* CreateVideoFrameAncillaryPacketsInstance (void)
|
||||||
|
{
|
||||||
|
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
|
||||||
|
|
||||||
|
if (gCreateVideoFrameAncillaryPacketsFunc == NULL)
|
||||||
|
return NULL;
|
||||||
|
return gCreateVideoFrameAncillaryPacketsFunc();
|
||||||
|
}
|
||||||
173
services/capture/sdk/DeckLinkAPIDispatch_v10_11.cpp
Normal file
173
services/capture/sdk/DeckLinkAPIDispatch_v10_11.cpp
Normal file
|
|
@ -0,0 +1,173 @@
|
||||||
|
/* -LICENSE-START-
|
||||||
|
** Copyright (c) 2019 Blackmagic Design
|
||||||
|
**
|
||||||
|
** Permission is hereby granted, free of charge, to any person or organization
|
||||||
|
** obtaining a copy of the software and accompanying documentation (the
|
||||||
|
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||||
|
** and transmit the Software, and to prepare derivative works of the Software,
|
||||||
|
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||||
|
** accordance with:
|
||||||
|
**
|
||||||
|
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||||
|
** Agreement for the Software Development Kit ("EULA") available at
|
||||||
|
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||||
|
**
|
||||||
|
** (2) if the Software is obtained from any third party, such licensing terms
|
||||||
|
** as notified by that third party,
|
||||||
|
**
|
||||||
|
** and all subject to the following:
|
||||||
|
**
|
||||||
|
** (3) the copyright notices in the Software and this entire statement,
|
||||||
|
** including the above license grant, this restriction and the following
|
||||||
|
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||||
|
** part, and all derivative works of the Software, unless such copies or
|
||||||
|
** derivative works are solely in the form of machine-executable object code
|
||||||
|
** generated by a source language processor.
|
||||||
|
**
|
||||||
|
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||||
|
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||||
|
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||||
|
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
** DEALINGS IN THE SOFTWARE.
|
||||||
|
**
|
||||||
|
** A copy of the Software is available free of charge at
|
||||||
|
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||||
|
**
|
||||||
|
** -LICENSE-END-
|
||||||
|
**/
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <dlfcn.h>
|
||||||
|
|
||||||
|
#include "DeckLinkAPI_v10_11.h"
|
||||||
|
|
||||||
|
#define kDeckLinkAPI_Name "libDeckLinkAPI.so"
|
||||||
|
#define KDeckLinkPreviewAPI_Name "libDeckLinkPreviewAPI.so"
|
||||||
|
|
||||||
|
typedef IDeckLinkIterator* (*CreateIteratorFunc)(void);
|
||||||
|
typedef IDeckLinkAPIInformation* (*CreateAPIInformationFunc)(void);
|
||||||
|
typedef IDeckLinkGLScreenPreviewHelper* (*CreateOpenGLScreenPreviewHelperFunc)(void);
|
||||||
|
typedef IDeckLinkVideoConversion* (*CreateVideoConversionInstanceFunc)(void);
|
||||||
|
typedef IDeckLinkDiscovery* (*CreateDeckLinkDiscoveryInstanceFunc)(void);
|
||||||
|
typedef IDeckLinkVideoFrameAncillaryPackets* (*CreateVideoFrameAncillaryPacketsInstanceFunc)(void);
|
||||||
|
|
||||||
|
static pthread_once_t gDeckLinkOnceControl = PTHREAD_ONCE_INIT;
|
||||||
|
static pthread_once_t gPreviewOnceControl = PTHREAD_ONCE_INIT;
|
||||||
|
|
||||||
|
static bool gLoadedDeckLinkAPI = false;
|
||||||
|
|
||||||
|
static CreateIteratorFunc gCreateIteratorFunc = NULL;
|
||||||
|
static CreateAPIInformationFunc gCreateAPIInformationFunc = NULL;
|
||||||
|
static CreateOpenGLScreenPreviewHelperFunc gCreateOpenGLPreviewFunc = NULL;
|
||||||
|
static CreateVideoConversionInstanceFunc gCreateVideoConversionFunc = NULL;
|
||||||
|
static CreateDeckLinkDiscoveryInstanceFunc gCreateDeckLinkDiscoveryFunc = NULL;
|
||||||
|
static CreateVideoFrameAncillaryPacketsInstanceFunc gCreateVideoFrameAncillaryPacketsFunc = NULL;
|
||||||
|
|
||||||
|
static void InitDeckLinkAPI (void)
|
||||||
|
{
|
||||||
|
void *libraryHandle;
|
||||||
|
|
||||||
|
libraryHandle = dlopen(kDeckLinkAPI_Name, RTLD_NOW|RTLD_GLOBAL);
|
||||||
|
if (!libraryHandle)
|
||||||
|
{
|
||||||
|
fprintf(stderr, "%s\n", dlerror());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
gLoadedDeckLinkAPI = true;
|
||||||
|
|
||||||
|
gCreateIteratorFunc = (CreateIteratorFunc)dlsym(libraryHandle, "CreateDeckLinkIteratorInstance_0003");
|
||||||
|
if (!gCreateIteratorFunc)
|
||||||
|
fprintf(stderr, "%s\n", dlerror());
|
||||||
|
gCreateAPIInformationFunc = (CreateAPIInformationFunc)dlsym(libraryHandle, "CreateDeckLinkAPIInformationInstance_0001");
|
||||||
|
if (!gCreateAPIInformationFunc)
|
||||||
|
fprintf(stderr, "%s\n", dlerror());
|
||||||
|
gCreateVideoConversionFunc = (CreateVideoConversionInstanceFunc)dlsym(libraryHandle, "CreateVideoConversionInstance_0001");
|
||||||
|
if (!gCreateVideoConversionFunc)
|
||||||
|
fprintf(stderr, "%s\n", dlerror());
|
||||||
|
gCreateDeckLinkDiscoveryFunc = (CreateDeckLinkDiscoveryInstanceFunc)dlsym(libraryHandle, "CreateDeckLinkDiscoveryInstance_0002");
|
||||||
|
if (!gCreateDeckLinkDiscoveryFunc)
|
||||||
|
fprintf(stderr, "%s\n", dlerror());
|
||||||
|
gCreateVideoFrameAncillaryPacketsFunc = (CreateVideoFrameAncillaryPacketsInstanceFunc)dlsym(libraryHandle, "CreateVideoFrameAncillaryPacketsInstance_0001");
|
||||||
|
if (!gCreateVideoFrameAncillaryPacketsFunc)
|
||||||
|
fprintf(stderr, "%s\n", dlerror());
|
||||||
|
}
|
||||||
|
|
||||||
|
static void InitDeckLinkPreviewAPI (void)
|
||||||
|
{
|
||||||
|
void *libraryHandle;
|
||||||
|
|
||||||
|
libraryHandle = dlopen(KDeckLinkPreviewAPI_Name, RTLD_NOW|RTLD_GLOBAL);
|
||||||
|
if (!libraryHandle)
|
||||||
|
{
|
||||||
|
fprintf(stderr, "%s\n", dlerror());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
gCreateOpenGLPreviewFunc = (CreateOpenGLScreenPreviewHelperFunc)dlsym(libraryHandle, "CreateOpenGLScreenPreviewHelper_0001");
|
||||||
|
if (!gCreateOpenGLPreviewFunc)
|
||||||
|
fprintf(stderr, "%s\n", dlerror());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsDeckLinkAPIPresent_v10_11 (void)
|
||||||
|
{
|
||||||
|
// If the DeckLink API dynamic library was successfully loaded, return this knowledge to the caller
|
||||||
|
return gLoadedDeckLinkAPI;
|
||||||
|
}
|
||||||
|
|
||||||
|
IDeckLinkIterator* CreateDeckLinkIteratorInstance_v10_11 (void)
|
||||||
|
{
|
||||||
|
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
|
||||||
|
|
||||||
|
if (gCreateIteratorFunc == NULL)
|
||||||
|
return NULL;
|
||||||
|
return gCreateIteratorFunc();
|
||||||
|
}
|
||||||
|
|
||||||
|
IDeckLinkAPIInformation* CreateDeckLinkAPIInformationInstance_v10_11 (void)
|
||||||
|
{
|
||||||
|
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
|
||||||
|
|
||||||
|
if (gCreateAPIInformationFunc == NULL)
|
||||||
|
return NULL;
|
||||||
|
return gCreateAPIInformationFunc();
|
||||||
|
}
|
||||||
|
|
||||||
|
IDeckLinkGLScreenPreviewHelper* CreateOpenGLScreenPreviewHelper_v10_11 (void)
|
||||||
|
{
|
||||||
|
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
|
||||||
|
pthread_once(&gPreviewOnceControl, InitDeckLinkPreviewAPI);
|
||||||
|
|
||||||
|
if (gCreateOpenGLPreviewFunc == NULL)
|
||||||
|
return NULL;
|
||||||
|
return gCreateOpenGLPreviewFunc();
|
||||||
|
}
|
||||||
|
|
||||||
|
IDeckLinkVideoConversion* CreateVideoConversionInstance_v10_11 (void)
|
||||||
|
{
|
||||||
|
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
|
||||||
|
|
||||||
|
if (gCreateVideoConversionFunc == NULL)
|
||||||
|
return NULL;
|
||||||
|
return gCreateVideoConversionFunc();
|
||||||
|
}
|
||||||
|
|
||||||
|
IDeckLinkDiscovery* CreateDeckLinkDiscoveryInstance_v10_11 (void)
|
||||||
|
{
|
||||||
|
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
|
||||||
|
|
||||||
|
if (gCreateDeckLinkDiscoveryFunc == NULL)
|
||||||
|
return NULL;
|
||||||
|
return gCreateDeckLinkDiscoveryFunc();
|
||||||
|
}
|
||||||
|
|
||||||
|
IDeckLinkVideoFrameAncillaryPackets* CreateVideoFrameAncillaryPacketsInstance_v10_11 (void)
|
||||||
|
{
|
||||||
|
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
|
||||||
|
|
||||||
|
if (gCreateVideoFrameAncillaryPacketsFunc == NULL)
|
||||||
|
return NULL;
|
||||||
|
return gCreateVideoFrameAncillaryPacketsFunc();
|
||||||
|
}
|
||||||
159
services/capture/sdk/DeckLinkAPIDispatch_v10_8.cpp
Normal file
159
services/capture/sdk/DeckLinkAPIDispatch_v10_8.cpp
Normal file
|
|
@ -0,0 +1,159 @@
|
||||||
|
/* -LICENSE-START-
|
||||||
|
** Copyright (c) 2009 Blackmagic Design
|
||||||
|
**
|
||||||
|
** Permission is hereby granted, free of charge, to any person or organization
|
||||||
|
** obtaining a copy of the software and accompanying documentation (the
|
||||||
|
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||||
|
** and transmit the Software, and to prepare derivative works of the Software,
|
||||||
|
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||||
|
** accordance with:
|
||||||
|
**
|
||||||
|
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||||
|
** Agreement for the Software Development Kit ("EULA") available at
|
||||||
|
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||||
|
**
|
||||||
|
** (2) if the Software is obtained from any third party, such licensing terms
|
||||||
|
** as notified by that third party,
|
||||||
|
**
|
||||||
|
** and all subject to the following:
|
||||||
|
**
|
||||||
|
** (3) the copyright notices in the Software and this entire statement,
|
||||||
|
** including the above license grant, this restriction and the following
|
||||||
|
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||||
|
** part, and all derivative works of the Software, unless such copies or
|
||||||
|
** derivative works are solely in the form of machine-executable object code
|
||||||
|
** generated by a source language processor.
|
||||||
|
**
|
||||||
|
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||||
|
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||||
|
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||||
|
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
** DEALINGS IN THE SOFTWARE.
|
||||||
|
**
|
||||||
|
** A copy of the Software is available free of charge at
|
||||||
|
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||||
|
**
|
||||||
|
** -LICENSE-END-
|
||||||
|
**/
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <dlfcn.h>
|
||||||
|
|
||||||
|
#include "DeckLinkAPI.h"
|
||||||
|
|
||||||
|
#define kDeckLinkAPI_Name "libDeckLinkAPI.so"
|
||||||
|
#define KDeckLinkPreviewAPI_Name "libDeckLinkPreviewAPI.so"
|
||||||
|
|
||||||
|
typedef IDeckLinkIterator* (*CreateIteratorFunc)(void);
|
||||||
|
typedef IDeckLinkAPIInformation* (*CreateAPIInformationFunc)(void);
|
||||||
|
typedef IDeckLinkGLScreenPreviewHelper* (*CreateOpenGLScreenPreviewHelperFunc)(void);
|
||||||
|
typedef IDeckLinkVideoConversion* (*CreateVideoConversionInstanceFunc)(void);
|
||||||
|
typedef IDeckLinkDiscovery* (*CreateDeckLinkDiscoveryInstanceFunc)(void);
|
||||||
|
|
||||||
|
static pthread_once_t gDeckLinkOnceControl = PTHREAD_ONCE_INIT;
|
||||||
|
static pthread_once_t gPreviewOnceControl = PTHREAD_ONCE_INIT;
|
||||||
|
|
||||||
|
static bool gLoadedDeckLinkAPI = false;
|
||||||
|
|
||||||
|
static CreateIteratorFunc gCreateIteratorFunc = NULL;
|
||||||
|
static CreateAPIInformationFunc gCreateAPIInformationFunc = NULL;
|
||||||
|
static CreateOpenGLScreenPreviewHelperFunc gCreateOpenGLPreviewFunc = NULL;
|
||||||
|
static CreateVideoConversionInstanceFunc gCreateVideoConversionFunc = NULL;
|
||||||
|
static CreateDeckLinkDiscoveryInstanceFunc gCreateDeckLinkDiscoveryFunc = NULL;
|
||||||
|
|
||||||
|
static void InitDeckLinkAPI(void)
|
||||||
|
{
|
||||||
|
void *libraryHandle;
|
||||||
|
|
||||||
|
libraryHandle = dlopen(kDeckLinkAPI_Name, RTLD_NOW | RTLD_GLOBAL);
|
||||||
|
if (!libraryHandle)
|
||||||
|
{
|
||||||
|
fprintf(stderr, "%s\n", dlerror());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
gLoadedDeckLinkAPI = true;
|
||||||
|
|
||||||
|
gCreateIteratorFunc = (CreateIteratorFunc)dlsym(libraryHandle, "CreateDeckLinkIteratorInstance_0002");
|
||||||
|
if (!gCreateIteratorFunc)
|
||||||
|
fprintf(stderr, "%s\n", dlerror());
|
||||||
|
gCreateAPIInformationFunc = (CreateAPIInformationFunc)dlsym(libraryHandle, "CreateDeckLinkAPIInformationInstance_0001");
|
||||||
|
if (!gCreateAPIInformationFunc)
|
||||||
|
fprintf(stderr, "%s\n", dlerror());
|
||||||
|
gCreateVideoConversionFunc = (CreateVideoConversionInstanceFunc)dlsym(libraryHandle, "CreateVideoConversionInstance_0001");
|
||||||
|
if (!gCreateVideoConversionFunc)
|
||||||
|
fprintf(stderr, "%s\n", dlerror());
|
||||||
|
gCreateDeckLinkDiscoveryFunc = (CreateDeckLinkDiscoveryInstanceFunc)dlsym(libraryHandle, "CreateDeckLinkDiscoveryInstance_0001");
|
||||||
|
if (!gCreateDeckLinkDiscoveryFunc)
|
||||||
|
fprintf(stderr, "%s\n", dlerror());
|
||||||
|
}
|
||||||
|
|
||||||
|
static void InitDeckLinkPreviewAPI(void)
|
||||||
|
{
|
||||||
|
void *libraryHandle;
|
||||||
|
|
||||||
|
libraryHandle = dlopen(KDeckLinkPreviewAPI_Name, RTLD_NOW | RTLD_GLOBAL);
|
||||||
|
if (!libraryHandle)
|
||||||
|
{
|
||||||
|
fprintf(stderr, "%s\n", dlerror());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
gCreateOpenGLPreviewFunc = (CreateOpenGLScreenPreviewHelperFunc)dlsym(libraryHandle, "CreateOpenGLScreenPreviewHelper_0001");
|
||||||
|
if (!gCreateOpenGLPreviewFunc)
|
||||||
|
fprintf(stderr, "%s\n", dlerror());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsDeckLinkAPIPresent(void)
|
||||||
|
{
|
||||||
|
// If the DeckLink API dynamic library was successfully loaded, return this knowledge to the caller
|
||||||
|
return gLoadedDeckLinkAPI;
|
||||||
|
}
|
||||||
|
|
||||||
|
IDeckLinkIterator* CreateDeckLinkIteratorInstance(void)
|
||||||
|
{
|
||||||
|
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
|
||||||
|
|
||||||
|
if (gCreateIteratorFunc == NULL)
|
||||||
|
return NULL;
|
||||||
|
return gCreateIteratorFunc();
|
||||||
|
}
|
||||||
|
|
||||||
|
IDeckLinkAPIInformation* CreateDeckLinkAPIInformationInstance(void)
|
||||||
|
{
|
||||||
|
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
|
||||||
|
|
||||||
|
if (gCreateAPIInformationFunc == NULL)
|
||||||
|
return NULL;
|
||||||
|
return gCreateAPIInformationFunc();
|
||||||
|
}
|
||||||
|
|
||||||
|
IDeckLinkGLScreenPreviewHelper* CreateOpenGLScreenPreviewHelper(void)
|
||||||
|
{
|
||||||
|
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
|
||||||
|
pthread_once(&gPreviewOnceControl, InitDeckLinkPreviewAPI);
|
||||||
|
|
||||||
|
if (gCreateOpenGLPreviewFunc == NULL)
|
||||||
|
return NULL;
|
||||||
|
return gCreateOpenGLPreviewFunc();
|
||||||
|
}
|
||||||
|
|
||||||
|
IDeckLinkVideoConversion* CreateVideoConversionInstance(void)
|
||||||
|
{
|
||||||
|
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
|
||||||
|
|
||||||
|
if (gCreateVideoConversionFunc == NULL)
|
||||||
|
return NULL;
|
||||||
|
return gCreateVideoConversionFunc();
|
||||||
|
}
|
||||||
|
|
||||||
|
IDeckLinkDiscovery* CreateDeckLinkDiscoveryInstance(void)
|
||||||
|
{
|
||||||
|
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
|
||||||
|
|
||||||
|
if (gCreateDeckLinkDiscoveryFunc == NULL)
|
||||||
|
return NULL;
|
||||||
|
return gCreateDeckLinkDiscoveryFunc();
|
||||||
|
}
|
||||||
188
services/capture/sdk/DeckLinkAPIDispatch_v14_2_1.cpp
Normal file
188
services/capture/sdk/DeckLinkAPIDispatch_v14_2_1.cpp
Normal file
|
|
@ -0,0 +1,188 @@
|
||||||
|
/* -LICENSE-START-
|
||||||
|
** Copyright (c) 2009 Blackmagic Design
|
||||||
|
**
|
||||||
|
** Permission is hereby granted, free of charge, to any person or organization
|
||||||
|
** obtaining a copy of the software and accompanying documentation (the
|
||||||
|
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||||
|
** and transmit the Software, and to prepare derivative works of the Software,
|
||||||
|
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||||
|
** accordance with:
|
||||||
|
**
|
||||||
|
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||||
|
** Agreement for the Software Development Kit (“EULA”) available at
|
||||||
|
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||||
|
**
|
||||||
|
** (2) if the Software is obtained from any third party, such licensing terms
|
||||||
|
** as notified by that third party,
|
||||||
|
**
|
||||||
|
** and all subject to the following:
|
||||||
|
**
|
||||||
|
** (3) the copyright notices in the Software and this entire statement,
|
||||||
|
** including the above license grant, this restriction and the following
|
||||||
|
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||||
|
** part, and all derivative works of the Software, unless such copies or
|
||||||
|
** derivative works are solely in the form of machine-executable object code
|
||||||
|
** generated by a source language processor.
|
||||||
|
**
|
||||||
|
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||||
|
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||||
|
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||||
|
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
** DEALINGS IN THE SOFTWARE.
|
||||||
|
**
|
||||||
|
** A copy of the Software is available free of charge at
|
||||||
|
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||||
|
**
|
||||||
|
** -LICENSE-END-
|
||||||
|
**/
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <dlfcn.h>
|
||||||
|
|
||||||
|
#include "DeckLinkAPI_v14_2_1.h"
|
||||||
|
|
||||||
|
#define kDeckLinkAPI_Name "libDeckLinkAPI.so"
|
||||||
|
#define KDeckLinkPreviewAPI_Name "libDeckLinkPreviewAPI.so"
|
||||||
|
|
||||||
|
typedef IDeckLinkIterator* (*CreateIteratorFunc)(void);
|
||||||
|
typedef IDeckLinkAPIInformation* (*CreateAPIInformationFunc)(void);
|
||||||
|
typedef IDeckLinkGLScreenPreviewHelper_v14_2_1* (*CreateOpenGLScreenPreviewHelperFunc)(void);
|
||||||
|
typedef IDeckLinkGLScreenPreviewHelper_v14_2_1* (*CreateOpenGL3ScreenPreviewHelperFunc)(void);
|
||||||
|
typedef IDeckLinkVideoConversion_v14_2_1* (*CreateVideoConversionInstanceFunc)(void);
|
||||||
|
typedef IDeckLinkDiscovery* (*CreateDeckLinkDiscoveryInstanceFunc)(void);
|
||||||
|
typedef IDeckLinkVideoFrameAncillaryPackets* (*CreateVideoFrameAncillaryPacketsInstanceFunc)(void);
|
||||||
|
|
||||||
|
static pthread_once_t gDeckLinkOnceControl = PTHREAD_ONCE_INIT;
|
||||||
|
static pthread_once_t gPreviewOnceControl = PTHREAD_ONCE_INIT;
|
||||||
|
|
||||||
|
static bool gLoadedDeckLinkAPI = false;
|
||||||
|
|
||||||
|
static CreateIteratorFunc gCreateIteratorFunc = NULL;
|
||||||
|
static CreateAPIInformationFunc gCreateAPIInformationFunc = NULL;
|
||||||
|
static CreateOpenGLScreenPreviewHelperFunc gCreateOpenGLPreviewFunc = NULL;
|
||||||
|
static CreateOpenGL3ScreenPreviewHelperFunc gCreateOpenGL3PreviewFunc = NULL;
|
||||||
|
static CreateVideoConversionInstanceFunc gCreateVideoConversionFunc = NULL;
|
||||||
|
static CreateDeckLinkDiscoveryInstanceFunc gCreateDeckLinkDiscoveryFunc = NULL;
|
||||||
|
static CreateVideoFrameAncillaryPacketsInstanceFunc gCreateVideoFrameAncillaryPacketsFunc = NULL;
|
||||||
|
|
||||||
|
static void InitDeckLinkAPI (void)
|
||||||
|
{
|
||||||
|
void *libraryHandle;
|
||||||
|
|
||||||
|
libraryHandle = dlopen(kDeckLinkAPI_Name, RTLD_NOW|RTLD_GLOBAL);
|
||||||
|
if (!libraryHandle)
|
||||||
|
{
|
||||||
|
fprintf(stderr, "%s\n", dlerror());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
gLoadedDeckLinkAPI = true;
|
||||||
|
|
||||||
|
gCreateIteratorFunc = (CreateIteratorFunc)dlsym(libraryHandle, "CreateDeckLinkIteratorInstance_0004");
|
||||||
|
if (!gCreateIteratorFunc)
|
||||||
|
fprintf(stderr, "%s\n", dlerror());
|
||||||
|
gCreateAPIInformationFunc = (CreateAPIInformationFunc)dlsym(libraryHandle, "CreateDeckLinkAPIInformationInstance_0001");
|
||||||
|
if (!gCreateAPIInformationFunc)
|
||||||
|
fprintf(stderr, "%s\n", dlerror());
|
||||||
|
gCreateVideoConversionFunc = (CreateVideoConversionInstanceFunc)dlsym(libraryHandle, "CreateVideoConversionInstance_0001");
|
||||||
|
if (!gCreateVideoConversionFunc)
|
||||||
|
fprintf(stderr, "%s\n", dlerror());
|
||||||
|
gCreateDeckLinkDiscoveryFunc = (CreateDeckLinkDiscoveryInstanceFunc)dlsym(libraryHandle, "CreateDeckLinkDiscoveryInstance_0003");
|
||||||
|
if (!gCreateDeckLinkDiscoveryFunc)
|
||||||
|
fprintf(stderr, "%s\n", dlerror());
|
||||||
|
gCreateVideoFrameAncillaryPacketsFunc = (CreateVideoFrameAncillaryPacketsInstanceFunc)dlsym(libraryHandle, "CreateVideoFrameAncillaryPacketsInstance_0001");
|
||||||
|
if (!gCreateVideoFrameAncillaryPacketsFunc)
|
||||||
|
fprintf(stderr, "%s\n", dlerror());
|
||||||
|
}
|
||||||
|
|
||||||
|
static void InitDeckLinkPreviewAPI (void)
|
||||||
|
{
|
||||||
|
void *libraryHandle;
|
||||||
|
|
||||||
|
libraryHandle = dlopen(KDeckLinkPreviewAPI_Name, RTLD_NOW|RTLD_GLOBAL);
|
||||||
|
if (!libraryHandle)
|
||||||
|
{
|
||||||
|
fprintf(stderr, "%s\n", dlerror());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
gCreateOpenGLPreviewFunc = (CreateOpenGLScreenPreviewHelperFunc)dlsym(libraryHandle, "CreateOpenGLScreenPreviewHelper_0001");
|
||||||
|
if (!gCreateOpenGLPreviewFunc)
|
||||||
|
fprintf(stderr, "%s\n", dlerror());
|
||||||
|
gCreateOpenGL3PreviewFunc = (CreateOpenGL3ScreenPreviewHelperFunc)dlsym(libraryHandle, "CreateOpenGL3ScreenPreviewHelper_0001");
|
||||||
|
if (!gCreateOpenGL3PreviewFunc)
|
||||||
|
fprintf(stderr, "%s\n", dlerror());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsDeckLinkAPIPresent_v14_2_1 (void)
|
||||||
|
{
|
||||||
|
// If the DeckLink API dynamic library was successfully loaded, return this knowledge to the caller
|
||||||
|
return gLoadedDeckLinkAPI;
|
||||||
|
}
|
||||||
|
|
||||||
|
IDeckLinkIterator* CreateDeckLinkIteratorInstance_v14_2_1 (void)
|
||||||
|
{
|
||||||
|
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
|
||||||
|
|
||||||
|
if (gCreateIteratorFunc == NULL)
|
||||||
|
return NULL;
|
||||||
|
return gCreateIteratorFunc();
|
||||||
|
}
|
||||||
|
|
||||||
|
IDeckLinkAPIInformation* CreateDeckLinkAPIInformationInstance_v14_2_1 (void)
|
||||||
|
{
|
||||||
|
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
|
||||||
|
|
||||||
|
if (gCreateAPIInformationFunc == NULL)
|
||||||
|
return NULL;
|
||||||
|
return gCreateAPIInformationFunc();
|
||||||
|
}
|
||||||
|
|
||||||
|
IDeckLinkGLScreenPreviewHelper_v14_2_1* CreateOpenGLScreenPreviewHelper_v14_2_1 (void)
|
||||||
|
{
|
||||||
|
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
|
||||||
|
pthread_once(&gPreviewOnceControl, InitDeckLinkPreviewAPI);
|
||||||
|
|
||||||
|
if (gCreateOpenGLPreviewFunc == NULL)
|
||||||
|
return NULL;
|
||||||
|
return gCreateOpenGLPreviewFunc();
|
||||||
|
}
|
||||||
|
|
||||||
|
IDeckLinkGLScreenPreviewHelper_v14_2_1* CreateOpenGL3ScreenPreviewHelper_v14_2_1 (void)
|
||||||
|
{
|
||||||
|
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
|
||||||
|
pthread_once(&gPreviewOnceControl, InitDeckLinkPreviewAPI);
|
||||||
|
|
||||||
|
if (gCreateOpenGL3PreviewFunc == NULL)
|
||||||
|
return NULL;
|
||||||
|
return gCreateOpenGL3PreviewFunc();
|
||||||
|
}
|
||||||
|
|
||||||
|
IDeckLinkVideoConversion_v14_2_1* CreateVideoConversionInstance_v14_2_1 (void)
|
||||||
|
{
|
||||||
|
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
|
||||||
|
|
||||||
|
if (gCreateVideoConversionFunc == NULL)
|
||||||
|
return NULL;
|
||||||
|
return gCreateVideoConversionFunc();
|
||||||
|
}
|
||||||
|
|
||||||
|
IDeckLinkDiscovery* CreateDeckLinkDiscoveryInstance_v14_2_1 (void)
|
||||||
|
{
|
||||||
|
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
|
||||||
|
|
||||||
|
if (gCreateDeckLinkDiscoveryFunc == NULL)
|
||||||
|
return NULL;
|
||||||
|
return gCreateDeckLinkDiscoveryFunc();
|
||||||
|
}
|
||||||
|
|
||||||
|
IDeckLinkVideoFrameAncillaryPackets* CreateVideoFrameAncillaryPacketsInstance_v14_2_1 (void)
|
||||||
|
{
|
||||||
|
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
|
||||||
|
|
||||||
|
if (gCreateVideoFrameAncillaryPacketsFunc == NULL)
|
||||||
|
return NULL;
|
||||||
|
return gCreateVideoFrameAncillaryPacketsFunc();
|
||||||
|
}
|
||||||
188
services/capture/sdk/DeckLinkAPIDispatch_v15_2.cpp
Normal file
188
services/capture/sdk/DeckLinkAPIDispatch_v15_2.cpp
Normal file
|
|
@ -0,0 +1,188 @@
|
||||||
|
/* -LICENSE-START-
|
||||||
|
** Copyright (c) 2009 Blackmagic Design
|
||||||
|
**
|
||||||
|
** Permission is hereby granted, free of charge, to any person or organization
|
||||||
|
** obtaining a copy of the software and accompanying documentation (the
|
||||||
|
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||||
|
** and transmit the Software, and to prepare derivative works of the Software,
|
||||||
|
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||||
|
** accordance with:
|
||||||
|
**
|
||||||
|
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||||
|
** Agreement for the Software Development Kit ("EULA") available at
|
||||||
|
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||||
|
**
|
||||||
|
** (2) if the Software is obtained from any third party, such licensing terms
|
||||||
|
** as notified by that third party,
|
||||||
|
**
|
||||||
|
** and all subject to the following:
|
||||||
|
**
|
||||||
|
** (3) the copyright notices in the Software and this entire statement,
|
||||||
|
** including the above license grant, this restriction and the following
|
||||||
|
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||||
|
** part, and all derivative works of the Software, unless such copies or
|
||||||
|
** derivative works are solely in the form of machine-executable object code
|
||||||
|
** generated by a source language processor.
|
||||||
|
**
|
||||||
|
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||||
|
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||||
|
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||||
|
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
** DEALINGS IN THE SOFTWARE.
|
||||||
|
**
|
||||||
|
** A copy of the Software is available free of charge at
|
||||||
|
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||||
|
**
|
||||||
|
** -LICENSE-END-
|
||||||
|
**/
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <dlfcn.h>
|
||||||
|
|
||||||
|
#include "DeckLinkAPI_v15_2.h"
|
||||||
|
|
||||||
|
#define kDeckLinkAPI_Name "libDeckLinkAPI.so"
|
||||||
|
#define KDeckLinkPreviewAPI_Name "libDeckLinkPreviewAPI.so"
|
||||||
|
|
||||||
|
typedef IDeckLinkIterator* (*CreateIteratorFunc)(void);
|
||||||
|
typedef IDeckLinkAPIInformation* (*CreateAPIInformationFunc)(void);
|
||||||
|
typedef IDeckLinkGLScreenPreviewHelper* (*CreateOpenGLScreenPreviewHelperFunc)(void);
|
||||||
|
typedef IDeckLinkGLScreenPreviewHelper* (*CreateOpenGL3ScreenPreviewHelperFunc)(void);
|
||||||
|
typedef IDeckLinkVideoConversion* (*CreateVideoConversionInstanceFunc)(void);
|
||||||
|
typedef IDeckLinkDiscovery* (*CreateDeckLinkDiscoveryInstanceFunc)(void);
|
||||||
|
typedef IDeckLinkVideoFrameAncillaryPackets_v15_2* (*CreateVideoFrameAncillaryPacketsInstanceFunc)(void);
|
||||||
|
|
||||||
|
static pthread_once_t gDeckLinkOnceControl = PTHREAD_ONCE_INIT;
|
||||||
|
static pthread_once_t gPreviewOnceControl = PTHREAD_ONCE_INIT;
|
||||||
|
|
||||||
|
static bool gLoadedDeckLinkAPI = false;
|
||||||
|
|
||||||
|
static CreateIteratorFunc gCreateIteratorFunc = NULL;
|
||||||
|
static CreateAPIInformationFunc gCreateAPIInformationFunc = NULL;
|
||||||
|
static CreateOpenGLScreenPreviewHelperFunc gCreateOpenGLPreviewFunc = NULL;
|
||||||
|
static CreateOpenGL3ScreenPreviewHelperFunc gCreateOpenGL3PreviewFunc = NULL;
|
||||||
|
static CreateVideoConversionInstanceFunc gCreateVideoConversionFunc = NULL;
|
||||||
|
static CreateDeckLinkDiscoveryInstanceFunc gCreateDeckLinkDiscoveryFunc = NULL;
|
||||||
|
static CreateVideoFrameAncillaryPacketsInstanceFunc gCreateVideoFrameAncillaryPacketsFunc = NULL;
|
||||||
|
|
||||||
|
static void InitDeckLinkAPI (void)
|
||||||
|
{
|
||||||
|
void *libraryHandle;
|
||||||
|
|
||||||
|
libraryHandle = dlopen(kDeckLinkAPI_Name, RTLD_NOW|RTLD_GLOBAL);
|
||||||
|
if (!libraryHandle)
|
||||||
|
{
|
||||||
|
fprintf(stderr, "%s\n", dlerror());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
gLoadedDeckLinkAPI = true;
|
||||||
|
|
||||||
|
gCreateIteratorFunc = (CreateIteratorFunc)dlsym(libraryHandle, "CreateDeckLinkIteratorInstance_0004");
|
||||||
|
if (!gCreateIteratorFunc)
|
||||||
|
fprintf(stderr, "%s\n", dlerror());
|
||||||
|
gCreateAPIInformationFunc = (CreateAPIInformationFunc)dlsym(libraryHandle, "CreateDeckLinkAPIInformationInstance_0001");
|
||||||
|
if (!gCreateAPIInformationFunc)
|
||||||
|
fprintf(stderr, "%s\n", dlerror());
|
||||||
|
gCreateVideoConversionFunc = (CreateVideoConversionInstanceFunc)dlsym(libraryHandle, "CreateVideoConversionInstance_0002");
|
||||||
|
if (!gCreateVideoConversionFunc)
|
||||||
|
fprintf(stderr, "%s\n", dlerror());
|
||||||
|
gCreateDeckLinkDiscoveryFunc = (CreateDeckLinkDiscoveryInstanceFunc)dlsym(libraryHandle, "CreateDeckLinkDiscoveryInstance_0003");
|
||||||
|
if (!gCreateDeckLinkDiscoveryFunc)
|
||||||
|
fprintf(stderr, "%s\n", dlerror());
|
||||||
|
gCreateVideoFrameAncillaryPacketsFunc = (CreateVideoFrameAncillaryPacketsInstanceFunc)dlsym(libraryHandle, "CreateVideoFrameAncillaryPacketsInstance_0001");
|
||||||
|
if (!gCreateVideoFrameAncillaryPacketsFunc)
|
||||||
|
fprintf(stderr, "%s\n", dlerror());
|
||||||
|
}
|
||||||
|
|
||||||
|
static void InitDeckLinkPreviewAPI (void)
|
||||||
|
{
|
||||||
|
void *libraryHandle;
|
||||||
|
|
||||||
|
libraryHandle = dlopen(KDeckLinkPreviewAPI_Name, RTLD_NOW|RTLD_GLOBAL);
|
||||||
|
if (!libraryHandle)
|
||||||
|
{
|
||||||
|
fprintf(stderr, "%s\n", dlerror());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
gCreateOpenGLPreviewFunc = (CreateOpenGLScreenPreviewHelperFunc)dlsym(libraryHandle, "CreateOpenGLScreenPreviewHelper_0002");
|
||||||
|
if (!gCreateOpenGLPreviewFunc)
|
||||||
|
fprintf(stderr, "%s\n", dlerror());
|
||||||
|
gCreateOpenGL3PreviewFunc = (CreateOpenGL3ScreenPreviewHelperFunc)dlsym(libraryHandle, "CreateOpenGL3ScreenPreviewHelper_0002");
|
||||||
|
if (!gCreateOpenGL3PreviewFunc)
|
||||||
|
fprintf(stderr, "%s\n", dlerror());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsDeckLinkAPIPresent_v15_2 (void)
|
||||||
|
{
|
||||||
|
// If the DeckLink API dynamic library was successfully loaded, return this knowledge to the caller
|
||||||
|
return gLoadedDeckLinkAPI;
|
||||||
|
}
|
||||||
|
|
||||||
|
IDeckLinkIterator* CreateDeckLinkIteratorInstance (void)
|
||||||
|
{
|
||||||
|
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
|
||||||
|
|
||||||
|
if (gCreateIteratorFunc == NULL)
|
||||||
|
return NULL;
|
||||||
|
return gCreateIteratorFunc();
|
||||||
|
}
|
||||||
|
|
||||||
|
IDeckLinkAPIInformation* CreateDeckLinkAPIInformationInstance (void)
|
||||||
|
{
|
||||||
|
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
|
||||||
|
|
||||||
|
if (gCreateAPIInformationFunc == NULL)
|
||||||
|
return NULL;
|
||||||
|
return gCreateAPIInformationFunc();
|
||||||
|
}
|
||||||
|
|
||||||
|
IDeckLinkGLScreenPreviewHelper* CreateOpenGLScreenPreviewHelper (void)
|
||||||
|
{
|
||||||
|
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
|
||||||
|
pthread_once(&gPreviewOnceControl, InitDeckLinkPreviewAPI);
|
||||||
|
|
||||||
|
if (gCreateOpenGLPreviewFunc == NULL)
|
||||||
|
return NULL;
|
||||||
|
return gCreateOpenGLPreviewFunc();
|
||||||
|
}
|
||||||
|
|
||||||
|
IDeckLinkGLScreenPreviewHelper* CreateOpenGL3ScreenPreviewHelper (void)
|
||||||
|
{
|
||||||
|
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
|
||||||
|
pthread_once(&gPreviewOnceControl, InitDeckLinkPreviewAPI);
|
||||||
|
|
||||||
|
if (gCreateOpenGL3PreviewFunc == NULL)
|
||||||
|
return NULL;
|
||||||
|
return gCreateOpenGL3PreviewFunc();
|
||||||
|
}
|
||||||
|
|
||||||
|
IDeckLinkVideoConversion* CreateVideoConversionInstance (void)
|
||||||
|
{
|
||||||
|
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
|
||||||
|
|
||||||
|
if (gCreateVideoConversionFunc == NULL)
|
||||||
|
return NULL;
|
||||||
|
return gCreateVideoConversionFunc();
|
||||||
|
}
|
||||||
|
|
||||||
|
IDeckLinkDiscovery* CreateDeckLinkDiscoveryInstance (void)
|
||||||
|
{
|
||||||
|
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
|
||||||
|
|
||||||
|
if (gCreateDeckLinkDiscoveryFunc == NULL)
|
||||||
|
return NULL;
|
||||||
|
return gCreateDeckLinkDiscoveryFunc();
|
||||||
|
}
|
||||||
|
|
||||||
|
IDeckLinkVideoFrameAncillaryPackets_v15_2* CreateVideoFrameAncillaryPacketsInstance_v15_2 (void)
|
||||||
|
{
|
||||||
|
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
|
||||||
|
|
||||||
|
if (gCreateVideoFrameAncillaryPacketsFunc == NULL)
|
||||||
|
return NULL;
|
||||||
|
return gCreateVideoFrameAncillaryPacketsFunc();
|
||||||
|
}
|
||||||
188
services/capture/sdk/DeckLinkAPIDispatch_v15_3_1.cpp
Normal file
188
services/capture/sdk/DeckLinkAPIDispatch_v15_3_1.cpp
Normal file
|
|
@ -0,0 +1,188 @@
|
||||||
|
/* -LICENSE-START-
|
||||||
|
** Copyright (c) 2009 Blackmagic Design
|
||||||
|
**
|
||||||
|
** Permission is hereby granted, free of charge, to any person or organization
|
||||||
|
** obtaining a copy of the software and accompanying documentation (the
|
||||||
|
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||||
|
** and transmit the Software, and to prepare derivative works of the Software,
|
||||||
|
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||||
|
** accordance with:
|
||||||
|
**
|
||||||
|
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||||
|
** Agreement for the Software Development Kit ("EULA") available at
|
||||||
|
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||||
|
**
|
||||||
|
** (2) if the Software is obtained from any third party, such licensing terms
|
||||||
|
** as notified by that third party,
|
||||||
|
**
|
||||||
|
** and all subject to the following:
|
||||||
|
**
|
||||||
|
** (3) the copyright notices in the Software and this entire statement,
|
||||||
|
** including the above license grant, this restriction and the following
|
||||||
|
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||||
|
** part, and all derivative works of the Software, unless such copies or
|
||||||
|
** derivative works are solely in the form of machine-executable object code
|
||||||
|
** generated by a source language processor.
|
||||||
|
**
|
||||||
|
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||||
|
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||||
|
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||||
|
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
** DEALINGS IN THE SOFTWARE.
|
||||||
|
**
|
||||||
|
** A copy of the Software is available free of charge at
|
||||||
|
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||||
|
**
|
||||||
|
** -LICENSE-END-
|
||||||
|
**/
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <dlfcn.h>
|
||||||
|
|
||||||
|
#include "DeckLinkAPI_v15_3_1.h"
|
||||||
|
|
||||||
|
#define kDeckLinkAPI_Name "libDeckLinkAPI.so"
|
||||||
|
#define KDeckLinkPreviewAPI_Name "libDeckLinkPreviewAPI.so"
|
||||||
|
|
||||||
|
typedef IDeckLinkIterator* (*CreateIteratorFunc)(void);
|
||||||
|
typedef IDeckLinkAPIInformation* (*CreateAPIInformationFunc)(void);
|
||||||
|
typedef IDeckLinkGLScreenPreviewHelper* (*CreateOpenGLScreenPreviewHelperFunc)(void);
|
||||||
|
typedef IDeckLinkGLScreenPreviewHelper* (*CreateOpenGL3ScreenPreviewHelperFunc)(void);
|
||||||
|
typedef IDeckLinkVideoConversion_v15_3_1* (*CreateVideoConversionInstanceFunc)(void);
|
||||||
|
typedef IDeckLinkDiscovery* (*CreateDeckLinkDiscoveryInstanceFunc)(void);
|
||||||
|
typedef IDeckLinkVideoFrameAncillaryPackets* (*CreateVideoFrameAncillaryPacketsInstanceFunc)(void);
|
||||||
|
|
||||||
|
static pthread_once_t gDeckLinkOnceControl = PTHREAD_ONCE_INIT;
|
||||||
|
static pthread_once_t gPreviewOnceControl = PTHREAD_ONCE_INIT;
|
||||||
|
|
||||||
|
static bool gLoadedDeckLinkAPI = false;
|
||||||
|
|
||||||
|
static CreateIteratorFunc gCreateIteratorFunc = NULL;
|
||||||
|
static CreateAPIInformationFunc gCreateAPIInformationFunc = NULL;
|
||||||
|
static CreateOpenGLScreenPreviewHelperFunc gCreateOpenGLPreviewFunc = NULL;
|
||||||
|
static CreateOpenGL3ScreenPreviewHelperFunc gCreateOpenGL3PreviewFunc = NULL;
|
||||||
|
static CreateVideoConversionInstanceFunc gCreateVideoConversionFunc = NULL;
|
||||||
|
static CreateDeckLinkDiscoveryInstanceFunc gCreateDeckLinkDiscoveryFunc = NULL;
|
||||||
|
static CreateVideoFrameAncillaryPacketsInstanceFunc gCreateVideoFrameAncillaryPacketsFunc = NULL;
|
||||||
|
|
||||||
|
static void InitDeckLinkAPI (void)
|
||||||
|
{
|
||||||
|
void *libraryHandle;
|
||||||
|
|
||||||
|
libraryHandle = dlopen(kDeckLinkAPI_Name, RTLD_NOW|RTLD_GLOBAL);
|
||||||
|
if (!libraryHandle)
|
||||||
|
{
|
||||||
|
fprintf(stderr, "%s\n", dlerror());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
gLoadedDeckLinkAPI = true;
|
||||||
|
|
||||||
|
gCreateIteratorFunc = (CreateIteratorFunc)dlsym(libraryHandle, "CreateDeckLinkIteratorInstance_0004");
|
||||||
|
if (!gCreateIteratorFunc)
|
||||||
|
fprintf(stderr, "%s\n", dlerror());
|
||||||
|
gCreateAPIInformationFunc = (CreateAPIInformationFunc)dlsym(libraryHandle, "CreateDeckLinkAPIInformationInstance_0001");
|
||||||
|
if (!gCreateAPIInformationFunc)
|
||||||
|
fprintf(stderr, "%s\n", dlerror());
|
||||||
|
gCreateVideoConversionFunc = (CreateVideoConversionInstanceFunc)dlsym(libraryHandle, "CreateVideoConversionInstance_0002");
|
||||||
|
if (!gCreateVideoConversionFunc)
|
||||||
|
fprintf(stderr, "%s\n", dlerror());
|
||||||
|
gCreateDeckLinkDiscoveryFunc = (CreateDeckLinkDiscoveryInstanceFunc)dlsym(libraryHandle, "CreateDeckLinkDiscoveryInstance_0003");
|
||||||
|
if (!gCreateDeckLinkDiscoveryFunc)
|
||||||
|
fprintf(stderr, "%s\n", dlerror());
|
||||||
|
gCreateVideoFrameAncillaryPacketsFunc = (CreateVideoFrameAncillaryPacketsInstanceFunc)dlsym(libraryHandle, "CreateVideoFrameAncillaryPacketsInstance_0001");
|
||||||
|
if (!gCreateVideoFrameAncillaryPacketsFunc)
|
||||||
|
fprintf(stderr, "%s\n", dlerror());
|
||||||
|
}
|
||||||
|
|
||||||
|
static void InitDeckLinkPreviewAPI (void)
|
||||||
|
{
|
||||||
|
void *libraryHandle;
|
||||||
|
|
||||||
|
libraryHandle = dlopen(KDeckLinkPreviewAPI_Name, RTLD_NOW|RTLD_GLOBAL);
|
||||||
|
if (!libraryHandle)
|
||||||
|
{
|
||||||
|
fprintf(stderr, "%s\n", dlerror());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
gCreateOpenGLPreviewFunc = (CreateOpenGLScreenPreviewHelperFunc)dlsym(libraryHandle, "CreateOpenGLScreenPreviewHelper_0002");
|
||||||
|
if (!gCreateOpenGLPreviewFunc)
|
||||||
|
fprintf(stderr, "%s\n", dlerror());
|
||||||
|
gCreateOpenGL3PreviewFunc = (CreateOpenGL3ScreenPreviewHelperFunc)dlsym(libraryHandle, "CreateOpenGL3ScreenPreviewHelper_0002");
|
||||||
|
if (!gCreateOpenGL3PreviewFunc)
|
||||||
|
fprintf(stderr, "%s\n", dlerror());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsDeckLinkAPIPresent (void)
|
||||||
|
{
|
||||||
|
// If the DeckLink API dynamic library was successfully loaded, return this knowledge to the caller
|
||||||
|
return gLoadedDeckLinkAPI;
|
||||||
|
}
|
||||||
|
|
||||||
|
IDeckLinkIterator* CreateDeckLinkIteratorInstance_v15_3_1 (void)
|
||||||
|
{
|
||||||
|
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
|
||||||
|
|
||||||
|
if (gCreateIteratorFunc == NULL)
|
||||||
|
return NULL;
|
||||||
|
return gCreateIteratorFunc();
|
||||||
|
}
|
||||||
|
|
||||||
|
IDeckLinkAPIInformation* CreateDeckLinkAPIInformationInstance_v15_3_1 (void)
|
||||||
|
{
|
||||||
|
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
|
||||||
|
|
||||||
|
if (gCreateAPIInformationFunc == NULL)
|
||||||
|
return NULL;
|
||||||
|
return gCreateAPIInformationFunc();
|
||||||
|
}
|
||||||
|
|
||||||
|
IDeckLinkGLScreenPreviewHelper* CreateOpenGLScreenPreviewHelper_v15_3_1 (void)
|
||||||
|
{
|
||||||
|
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
|
||||||
|
pthread_once(&gPreviewOnceControl, InitDeckLinkPreviewAPI);
|
||||||
|
|
||||||
|
if (gCreateOpenGLPreviewFunc == NULL)
|
||||||
|
return NULL;
|
||||||
|
return gCreateOpenGLPreviewFunc();
|
||||||
|
}
|
||||||
|
|
||||||
|
IDeckLinkGLScreenPreviewHelper* CreateOpenGL3ScreenPreviewHelper_v15_3_1 (void)
|
||||||
|
{
|
||||||
|
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
|
||||||
|
pthread_once(&gPreviewOnceControl, InitDeckLinkPreviewAPI);
|
||||||
|
|
||||||
|
if (gCreateOpenGL3PreviewFunc == NULL)
|
||||||
|
return NULL;
|
||||||
|
return gCreateOpenGL3PreviewFunc();
|
||||||
|
}
|
||||||
|
|
||||||
|
IDeckLinkVideoConversion_v15_3_1* CreateVideoConversionInstance_v15_3_1 (void)
|
||||||
|
{
|
||||||
|
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
|
||||||
|
|
||||||
|
if (gCreateVideoConversionFunc == NULL)
|
||||||
|
return NULL;
|
||||||
|
return gCreateVideoConversionFunc();
|
||||||
|
}
|
||||||
|
|
||||||
|
IDeckLinkDiscovery* CreateDeckLinkDiscoveryInstance_v15_3_1 (void)
|
||||||
|
{
|
||||||
|
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
|
||||||
|
|
||||||
|
if (gCreateDeckLinkDiscoveryFunc == NULL)
|
||||||
|
return NULL;
|
||||||
|
return gCreateDeckLinkDiscoveryFunc();
|
||||||
|
}
|
||||||
|
|
||||||
|
IDeckLinkVideoFrameAncillaryPackets* CreateVideoFrameAncillaryPacketsInstance_v15_3_1 (void)
|
||||||
|
{
|
||||||
|
pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI);
|
||||||
|
|
||||||
|
if (gCreateVideoFrameAncillaryPacketsFunc == NULL)
|
||||||
|
return NULL;
|
||||||
|
return gCreateVideoFrameAncillaryPacketsFunc();
|
||||||
|
}
|
||||||
68
services/capture/sdk/DeckLinkAPIGLScreenPreview_v14_2_1.h
Normal file
68
services/capture/sdk/DeckLinkAPIGLScreenPreview_v14_2_1.h
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
/* -LICENSE-START-
|
||||||
|
** Copyright (c) 2022 Blackmagic Design
|
||||||
|
**
|
||||||
|
** Permission is hereby granted, free of charge, to any person or organization
|
||||||
|
** obtaining a copy of the software and accompanying documentation (the
|
||||||
|
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||||
|
** and transmit the Software, and to prepare derivative works of the Software,
|
||||||
|
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||||
|
** accordance with:
|
||||||
|
**
|
||||||
|
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||||
|
** Agreement for the Software Development Kit (“EULA”) available at
|
||||||
|
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||||
|
**
|
||||||
|
** (2) if the Software is obtained from any third party, such licensing terms
|
||||||
|
** as notified by that third party,
|
||||||
|
**
|
||||||
|
** and all subject to the following:
|
||||||
|
**
|
||||||
|
** (3) the copyright notices in the Software and this entire statement,
|
||||||
|
** including the above license grant, this restriction and the following
|
||||||
|
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||||
|
** part, and all derivative works of the Software, unless such copies or
|
||||||
|
** derivative works are solely in the form of machine-executable object code
|
||||||
|
** generated by a source language processor.
|
||||||
|
**
|
||||||
|
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||||
|
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||||
|
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||||
|
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
** DEALINGS IN THE SOFTWARE.
|
||||||
|
**
|
||||||
|
** A copy of the Software is available free of charge at
|
||||||
|
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||||
|
**
|
||||||
|
** -LICENSE-END-
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef BMD_DECKLINKAPIGLSCREENPREVIEW_v14_2_1_H
|
||||||
|
#define BMD_DECKLINKAPIGLSCREENPREVIEW_v14_2_1_H
|
||||||
|
|
||||||
|
#include "DeckLinkAPI.h"
|
||||||
|
#include "DeckLinkAPIVideoFrame_v14_2_1.h"
|
||||||
|
|
||||||
|
// Type Declarations
|
||||||
|
|
||||||
|
BMD_CONST REFIID IID_IDeckLinkGLScreenPreviewHelper_v14_2_1 = /* 504E2209-CAC7-4C1A-9FB4-C5BB6274D22F */ { 0x50, 0x4E, 0x22, 0x09, 0xCA, 0xC7, 0x4C, 0x1A, 0x9F, 0xB4, 0xC5, 0xBB, 0x62, 0x74, 0xD2, 0x2F };
|
||||||
|
|
||||||
|
/* Interface IDeckLinkGLScreenPreviewHelper - Created with CoCreateInstance on platforms with native COM support or from CreateOpenGLScreenPreviewHelper/CreateOpenGL3ScreenPreviewHelper on other platforms. */
|
||||||
|
|
||||||
|
class BMD_PUBLIC IDeckLinkGLScreenPreviewHelper_v14_2_1 : public IUnknown
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
|
||||||
|
/* Methods must be called with OpenGL context set */
|
||||||
|
|
||||||
|
virtual HRESULT InitializeGL (void) = 0;
|
||||||
|
virtual HRESULT PaintGL (void) = 0;
|
||||||
|
virtual HRESULT SetFrame (/* in */ IDeckLinkVideoFrame_v14_2_1* theFrame) = 0;
|
||||||
|
virtual HRESULT Set3DPreviewFormat (/* in */ BMD3DPreviewFormat previewFormat) = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual ~IDeckLinkGLScreenPreviewHelper_v14_2_1 () {} // call Release method to drop reference count
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
61
services/capture/sdk/DeckLinkAPIMemoryAllocator_v14_2_1.h
Normal file
61
services/capture/sdk/DeckLinkAPIMemoryAllocator_v14_2_1.h
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
/* -LICENSE-START-
|
||||||
|
** Copyright (c) 2022 Blackmagic Design
|
||||||
|
**
|
||||||
|
** Permission is hereby granted, free of charge, to any person or organization
|
||||||
|
** obtaining a copy of the software and accompanying documentation (the
|
||||||
|
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||||
|
** and transmit the Software, and to prepare derivative works of the Software,
|
||||||
|
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||||
|
** accordance with:
|
||||||
|
**
|
||||||
|
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||||
|
** Agreement for the Software Development Kit (“EULA”) available at
|
||||||
|
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||||
|
**
|
||||||
|
** (2) if the Software is obtained from any third party, such licensing terms
|
||||||
|
** as notified by that third party,
|
||||||
|
**
|
||||||
|
** and all subject to the following:
|
||||||
|
**
|
||||||
|
** (3) the copyright notices in the Software and this entire statement,
|
||||||
|
** including the above license grant, this restriction and the following
|
||||||
|
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||||
|
** part, and all derivative works of the Software, unless such copies or
|
||||||
|
** derivative works are solely in the form of machine-executable object code
|
||||||
|
** generated by a source language processor.
|
||||||
|
**
|
||||||
|
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||||
|
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||||
|
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||||
|
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
** DEALINGS IN THE SOFTWARE.
|
||||||
|
**
|
||||||
|
** A copy of the Software is available free of charge at
|
||||||
|
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||||
|
**
|
||||||
|
** -LICENSE-END-
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef BMD_DECKLINKAPIMEMORYALLOCATOR_v14_2_1_H
|
||||||
|
#define BMD_DECKLINKAPIMEMORYALLOCATOR_v14_2_1_H
|
||||||
|
|
||||||
|
#include "DeckLinkAPI.h"
|
||||||
|
|
||||||
|
// Type Declarations
|
||||||
|
|
||||||
|
BMD_CONST REFIID IID_IDeckLinkMemoryAllocator_v14_2_1 = /* B36EB6E7-9D29-4AA8-92EF-843B87A289E8 */ { 0xB3, 0x6E, 0xB6, 0xE7, 0x9D, 0x29, 0x4A, 0xA8, 0x92, 0xEF, 0x84, 0x3B, 0x87, 0xA2, 0x89, 0xE8 };
|
||||||
|
|
||||||
|
/* Interface IDeckLinkMemoryAllocator_v14_2_1 - Created with CoCreateInstance. */
|
||||||
|
|
||||||
|
class BMD_PUBLIC IDeckLinkMemoryAllocator_v14_2_1 : public IUnknown
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual HRESULT AllocateBuffer (/* in */ uint32_t bufferSize, /* out */ void** allocatedBuffer) = 0;
|
||||||
|
virtual HRESULT ReleaseBuffer (/* in */ void* buffer) = 0;
|
||||||
|
virtual HRESULT Commit (void) = 0;
|
||||||
|
virtual HRESULT Decommit (void) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
65
services/capture/sdk/DeckLinkAPIMetalScreenPreview_v14_2_1.h
Normal file
65
services/capture/sdk/DeckLinkAPIMetalScreenPreview_v14_2_1.h
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
/* -LICENSE-START-
|
||||||
|
** Copyright (c) 2022 Blackmagic Design
|
||||||
|
**
|
||||||
|
** Permission is hereby granted, free of charge, to any person or organization
|
||||||
|
** obtaining a copy of the software and accompanying documentation (the
|
||||||
|
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||||
|
** and transmit the Software, and to prepare derivative works of the Software,
|
||||||
|
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||||
|
** accordance with:
|
||||||
|
**
|
||||||
|
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||||
|
** Agreement for the Software Development Kit (“EULA”) available at
|
||||||
|
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||||
|
**
|
||||||
|
** (2) if the Software is obtained from any third party, such licensing terms
|
||||||
|
** as notified by that third party,
|
||||||
|
**
|
||||||
|
** and all subject to the following:
|
||||||
|
**
|
||||||
|
** (3) the copyright notices in the Software and this entire statement,
|
||||||
|
** including the above license grant, this restriction and the following
|
||||||
|
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||||
|
** part, and all derivative works of the Software, unless such copies or
|
||||||
|
** derivative works are solely in the form of machine-executable object code
|
||||||
|
** generated by a source language processor.
|
||||||
|
**
|
||||||
|
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||||
|
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||||
|
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||||
|
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
** DEALINGS IN THE SOFTWARE.
|
||||||
|
**
|
||||||
|
** A copy of the Software is available free of charge at
|
||||||
|
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||||
|
**
|
||||||
|
** -LICENSE-END-
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef BMD_DECKLINKAPIMETALSCREENPREVIEW_v14_2_1_H
|
||||||
|
#define BMD_DECKLINKAPIMETALSCREENPREVIEW_v14_2_1_H
|
||||||
|
|
||||||
|
#include "DeckLinkAPI.h"
|
||||||
|
#include "DeckLinkAPIVideoFrame_v14_2_1.h"
|
||||||
|
|
||||||
|
// Type Declarations
|
||||||
|
|
||||||
|
BMD_CONST REFIID IID_IDeckLinkMetalScreenPreviewHelper_v14_2_1 = /* 1AB252C5-DACB-4AE8-A58B-5320DE9CE373 */ { 0x1A, 0xB2, 0x52, 0xC5, 0xDA, 0xCB, 0x4A, 0xE8, 0xA5, 0x8B, 0x53, 0x20, 0xDE, 0x9C, 0xE3, 0x73 };
|
||||||
|
|
||||||
|
/* Interface IDeckLinkMetalScreenPreviewHelper - Created with CreateMetalScreenPreviewHelper(). */
|
||||||
|
|
||||||
|
class BMD_PUBLIC IDeckLinkMetalScreenPreviewHelper_v14_2_1 : public IUnknown
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual HRESULT Initialize (/* in */ void* device) = 0;
|
||||||
|
virtual HRESULT Draw (/* in */ void* cmdBuffer, /* in */ void* renderPassDescriptor, /* in */ void* viewport) = 0;
|
||||||
|
virtual HRESULT SetFrame (/* in */ IDeckLinkVideoFrame_v14_2_1* theFrame) = 0;
|
||||||
|
virtual HRESULT Set3DPreviewFormat (/* in */ BMD3DPreviewFormat previewFormat) = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual ~IDeckLinkMetalScreenPreviewHelper_v14_2_1 () {} // call Release method to drop reference count
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
291
services/capture/sdk/DeckLinkAPIModes.h
Normal file
291
services/capture/sdk/DeckLinkAPIModes.h
Normal file
|
|
@ -0,0 +1,291 @@
|
||||||
|
/* -LICENSE-START-
|
||||||
|
** Copyright (c) 2026 Blackmagic Design
|
||||||
|
**
|
||||||
|
** Permission is hereby granted, free of charge, to any person or organization
|
||||||
|
** obtaining a copy of the software and accompanying documentation covered by
|
||||||
|
** this license (the "Software") to use, reproduce, display, distribute,
|
||||||
|
** execute, and transmit the Software, and to prepare derivative works of the
|
||||||
|
** Software, and to permit third-parties to whom the Software is furnished to
|
||||||
|
** do so, all subject to the following:
|
||||||
|
**
|
||||||
|
** The copyright notices in the Software and this entire statement, including
|
||||||
|
** the above license grant, this restriction and the following disclaimer,
|
||||||
|
** must be included in all copies of the Software, in whole or in part, and
|
||||||
|
** all derivative works of the Software, unless such copies or derivative
|
||||||
|
** works are solely in the form of machine-executable object code generated by
|
||||||
|
** a source language processor.
|
||||||
|
**
|
||||||
|
** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||||
|
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||||
|
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||||
|
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
** DEALINGS IN THE SOFTWARE.
|
||||||
|
** -LICENSE-END-
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -- AUTOMATICALLY GENERATED - DO NOT EDIT ---
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef BMD_DECKLINKAPIMODES_H
|
||||||
|
#define BMD_DECKLINKAPIMODES_H
|
||||||
|
|
||||||
|
|
||||||
|
#ifndef BMD_CONST
|
||||||
|
#if defined(_MSC_VER)
|
||||||
|
#define BMD_CONST __declspec(selectany) static const
|
||||||
|
#else
|
||||||
|
#define BMD_CONST static const
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef BMD_PUBLIC
|
||||||
|
#define BMD_PUBLIC
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Type Declarations
|
||||||
|
|
||||||
|
|
||||||
|
// Interface ID Declarations
|
||||||
|
|
||||||
|
BMD_CONST REFIID IID_IDeckLinkDisplayModeIterator = /* 9C88499F-F601-4021-B80B-032E4EB41C35 */ { 0x9C,0x88,0x49,0x9F,0xF6,0x01,0x40,0x21,0xB8,0x0B,0x03,0x2E,0x4E,0xB4,0x1C,0x35 };
|
||||||
|
BMD_CONST REFIID IID_IDeckLinkDisplayMode = /* 3EB2C1AB-0A3D-4523-A3AD-F40D7FB14E78 */ { 0x3E,0xB2,0xC1,0xAB,0x0A,0x3D,0x45,0x23,0xA3,0xAD,0xF4,0x0D,0x7F,0xB1,0x4E,0x78 };
|
||||||
|
|
||||||
|
/* Enum BMDDisplayMode - BMDDisplayMode enumerates the video modes supported. */
|
||||||
|
|
||||||
|
typedef uint32_t BMDDisplayMode;
|
||||||
|
enum _BMDDisplayMode {
|
||||||
|
|
||||||
|
/* SD Modes */
|
||||||
|
|
||||||
|
bmdModeNTSC = /* 'ntsc' */ 0x6E747363,
|
||||||
|
bmdModeNTSC2398 = /* 'nt23' */ 0x6E743233, // 3:2 pulldown
|
||||||
|
bmdModePAL = /* 'pal ' */ 0x70616C20,
|
||||||
|
bmdModeNTSCp = /* 'ntsp' */ 0x6E747370,
|
||||||
|
bmdModePALp = /* 'palp' */ 0x70616C70,
|
||||||
|
|
||||||
|
/* HD 1080 Modes */
|
||||||
|
|
||||||
|
bmdModeHD1080p2398 = /* '23ps' */ 0x32337073,
|
||||||
|
bmdModeHD1080p24 = /* '24ps' */ 0x32347073,
|
||||||
|
bmdModeHD1080p25 = /* 'Hp25' */ 0x48703235,
|
||||||
|
bmdModeHD1080p2997 = /* 'Hp29' */ 0x48703239,
|
||||||
|
bmdModeHD1080p30 = /* 'Hp30' */ 0x48703330,
|
||||||
|
bmdModeHD1080p4795 = /* 'Hp47' */ 0x48703437,
|
||||||
|
bmdModeHD1080p48 = /* 'Hp48' */ 0x48703438,
|
||||||
|
bmdModeHD1080p50 = /* 'Hp50' */ 0x48703530,
|
||||||
|
bmdModeHD1080p5994 = /* 'Hp59' */ 0x48703539,
|
||||||
|
bmdModeHD1080p6000 = /* 'Hp60' */ 0x48703630, // N.B. This _really_ is 60.00 Hz.
|
||||||
|
bmdModeHD1080p9590 = /* 'Hp95' */ 0x48703935,
|
||||||
|
bmdModeHD1080p96 = /* 'Hp96' */ 0x48703936,
|
||||||
|
bmdModeHD1080p100 = /* 'Hp10' */ 0x48703130,
|
||||||
|
bmdModeHD1080p11988 = /* 'Hp11' */ 0x48703131,
|
||||||
|
bmdModeHD1080p120 = /* 'Hp12' */ 0x48703132,
|
||||||
|
bmdModeHD1080i50 = /* 'Hi50' */ 0x48693530,
|
||||||
|
bmdModeHD1080i5994 = /* 'Hi59' */ 0x48693539,
|
||||||
|
bmdModeHD1080i6000 = /* 'Hi60' */ 0x48693630, // N.B. This _really_ is 60.00 Hz.
|
||||||
|
|
||||||
|
/* HD 720 Modes */
|
||||||
|
|
||||||
|
bmdModeHD720p50 = /* 'hp50' */ 0x68703530,
|
||||||
|
bmdModeHD720p5994 = /* 'hp59' */ 0x68703539,
|
||||||
|
bmdModeHD720p60 = /* 'hp60' */ 0x68703630,
|
||||||
|
|
||||||
|
/* 2K Modes */
|
||||||
|
|
||||||
|
bmdMode2k2398 = /* '2k23' */ 0x326B3233,
|
||||||
|
bmdMode2k24 = /* '2k24' */ 0x326B3234,
|
||||||
|
bmdMode2k25 = /* '2k25' */ 0x326B3235,
|
||||||
|
|
||||||
|
/* 2K DCI Modes */
|
||||||
|
|
||||||
|
bmdMode2kDCI2398 = /* '2d23' */ 0x32643233,
|
||||||
|
bmdMode2kDCI24 = /* '2d24' */ 0x32643234,
|
||||||
|
bmdMode2kDCI25 = /* '2d25' */ 0x32643235,
|
||||||
|
bmdMode2kDCI2997 = /* '2d29' */ 0x32643239,
|
||||||
|
bmdMode2kDCI30 = /* '2d30' */ 0x32643330,
|
||||||
|
bmdMode2kDCI4795 = /* '2d47' */ 0x32643437,
|
||||||
|
bmdMode2kDCI48 = /* '2d48' */ 0x32643438,
|
||||||
|
bmdMode2kDCI50 = /* '2d50' */ 0x32643530,
|
||||||
|
bmdMode2kDCI5994 = /* '2d59' */ 0x32643539,
|
||||||
|
bmdMode2kDCI60 = /* '2d60' */ 0x32643630,
|
||||||
|
bmdMode2kDCI9590 = /* '2d95' */ 0x32643935,
|
||||||
|
bmdMode2kDCI96 = /* '2d96' */ 0x32643936,
|
||||||
|
bmdMode2kDCI100 = /* '2d10' */ 0x32643130,
|
||||||
|
bmdMode2kDCI11988 = /* '2d11' */ 0x32643131,
|
||||||
|
bmdMode2kDCI120 = /* '2d12' */ 0x32643132,
|
||||||
|
|
||||||
|
/* 4K UHD Modes */
|
||||||
|
|
||||||
|
bmdMode4K2160p2398 = /* '4k23' */ 0x346B3233,
|
||||||
|
bmdMode4K2160p24 = /* '4k24' */ 0x346B3234,
|
||||||
|
bmdMode4K2160p25 = /* '4k25' */ 0x346B3235,
|
||||||
|
bmdMode4K2160p2997 = /* '4k29' */ 0x346B3239,
|
||||||
|
bmdMode4K2160p30 = /* '4k30' */ 0x346B3330,
|
||||||
|
bmdMode4K2160p4795 = /* '4k47' */ 0x346B3437,
|
||||||
|
bmdMode4K2160p48 = /* '4k48' */ 0x346B3438,
|
||||||
|
bmdMode4K2160p50 = /* '4k50' */ 0x346B3530,
|
||||||
|
bmdMode4K2160p5994 = /* '4k59' */ 0x346B3539,
|
||||||
|
bmdMode4K2160p60 = /* '4k60' */ 0x346B3630,
|
||||||
|
bmdMode4K2160p9590 = /* '4k95' */ 0x346B3935,
|
||||||
|
bmdMode4K2160p96 = /* '4k96' */ 0x346B3936,
|
||||||
|
bmdMode4K2160p100 = /* '4k10' */ 0x346B3130,
|
||||||
|
bmdMode4K2160p11988 = /* '4k11' */ 0x346B3131,
|
||||||
|
bmdMode4K2160p120 = /* '4k12' */ 0x346B3132,
|
||||||
|
|
||||||
|
/* 4K DCI Modes */
|
||||||
|
|
||||||
|
bmdMode4kDCI2398 = /* '4d23' */ 0x34643233,
|
||||||
|
bmdMode4kDCI24 = /* '4d24' */ 0x34643234,
|
||||||
|
bmdMode4kDCI25 = /* '4d25' */ 0x34643235,
|
||||||
|
bmdMode4kDCI2997 = /* '4d29' */ 0x34643239,
|
||||||
|
bmdMode4kDCI30 = /* '4d30' */ 0x34643330,
|
||||||
|
bmdMode4kDCI4795 = /* '4d47' */ 0x34643437,
|
||||||
|
bmdMode4kDCI48 = /* '4d48' */ 0x34643438,
|
||||||
|
bmdMode4kDCI50 = /* '4d50' */ 0x34643530,
|
||||||
|
bmdMode4kDCI5994 = /* '4d59' */ 0x34643539,
|
||||||
|
bmdMode4kDCI60 = /* '4d60' */ 0x34643630,
|
||||||
|
bmdMode4kDCI9590 = /* '4d95' */ 0x34643935,
|
||||||
|
bmdMode4kDCI96 = /* '4d96' */ 0x34643936,
|
||||||
|
bmdMode4kDCI100 = /* '4d10' */ 0x34643130,
|
||||||
|
bmdMode4kDCI11988 = /* '4d11' */ 0x34643131,
|
||||||
|
bmdMode4kDCI120 = /* '4d12' */ 0x34643132,
|
||||||
|
|
||||||
|
/* 8K UHD Modes */
|
||||||
|
|
||||||
|
bmdMode8K4320p2398 = /* '8k23' */ 0x386B3233,
|
||||||
|
bmdMode8K4320p24 = /* '8k24' */ 0x386B3234,
|
||||||
|
bmdMode8K4320p25 = /* '8k25' */ 0x386B3235,
|
||||||
|
bmdMode8K4320p2997 = /* '8k29' */ 0x386B3239,
|
||||||
|
bmdMode8K4320p30 = /* '8k30' */ 0x386B3330,
|
||||||
|
bmdMode8K4320p4795 = /* '8k47' */ 0x386B3437,
|
||||||
|
bmdMode8K4320p48 = /* '8k48' */ 0x386B3438,
|
||||||
|
bmdMode8K4320p50 = /* '8k50' */ 0x386B3530,
|
||||||
|
bmdMode8K4320p5994 = /* '8k59' */ 0x386B3539,
|
||||||
|
bmdMode8K4320p60 = /* '8k60' */ 0x386B3630,
|
||||||
|
|
||||||
|
/* 8K DCI Modes */
|
||||||
|
|
||||||
|
bmdMode8kDCI2398 = /* '8d23' */ 0x38643233,
|
||||||
|
bmdMode8kDCI24 = /* '8d24' */ 0x38643234,
|
||||||
|
bmdMode8kDCI25 = /* '8d25' */ 0x38643235,
|
||||||
|
bmdMode8kDCI2997 = /* '8d29' */ 0x38643239,
|
||||||
|
bmdMode8kDCI30 = /* '8d30' */ 0x38643330,
|
||||||
|
bmdMode8kDCI4795 = /* '8d47' */ 0x38643437,
|
||||||
|
bmdMode8kDCI48 = /* '8d48' */ 0x38643438,
|
||||||
|
bmdMode8kDCI50 = /* '8d50' */ 0x38643530,
|
||||||
|
bmdMode8kDCI5994 = /* '8d59' */ 0x38643539,
|
||||||
|
bmdMode8kDCI60 = /* '8d60' */ 0x38643630,
|
||||||
|
|
||||||
|
/* PC Modes */
|
||||||
|
|
||||||
|
bmdMode640x480p60 = /* 'vga6' */ 0x76676136,
|
||||||
|
bmdMode800x600p60 = /* 'svg6' */ 0x73766736,
|
||||||
|
bmdMode1440x900p50 = /* 'wxg5' */ 0x77786735,
|
||||||
|
bmdMode1440x900p60 = /* 'wxg6' */ 0x77786736,
|
||||||
|
bmdMode1440x1080p50 = /* 'sxg5' */ 0x73786735,
|
||||||
|
bmdMode1440x1080p60 = /* 'sxg6' */ 0x73786736,
|
||||||
|
bmdMode1600x1200p50 = /* 'uxg5' */ 0x75786735,
|
||||||
|
bmdMode1600x1200p60 = /* 'uxg6' */ 0x75786736,
|
||||||
|
bmdMode1920x1200p50 = /* 'wux5' */ 0x77757835,
|
||||||
|
bmdMode1920x1200p60 = /* 'wux6' */ 0x77757836,
|
||||||
|
bmdMode1920x1440p50 = /* '1945' */ 0x31393435,
|
||||||
|
bmdMode1920x1440p60 = /* '1946' */ 0x31393436,
|
||||||
|
bmdMode2560x1440p50 = /* 'wqh5' */ 0x77716835,
|
||||||
|
bmdMode2560x1440p60 = /* 'wqh6' */ 0x77716836,
|
||||||
|
bmdMode2560x1600p50 = /* 'wqx5' */ 0x77717835,
|
||||||
|
bmdMode2560x1600p60 = /* 'wqx6' */ 0x77717836,
|
||||||
|
bmdModeUnknown = /* 'iunk' */ 0x69756E6B
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Enum BMDFieldDominance - BMDFieldDominance enumerates settings applicable to video fields. */
|
||||||
|
|
||||||
|
typedef uint32_t BMDFieldDominance;
|
||||||
|
enum _BMDFieldDominance {
|
||||||
|
bmdUnknownFieldDominance = 0,
|
||||||
|
bmdLowerFieldFirst = /* 'lowr' */ 0x6C6F7772,
|
||||||
|
bmdUpperFieldFirst = /* 'uppr' */ 0x75707072,
|
||||||
|
bmdProgressiveFrame = /* 'prog' */ 0x70726F67,
|
||||||
|
bmdProgressiveSegmentedFrame = /* 'psf ' */ 0x70736620
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Enum BMDPixelFormat - Video pixel formats supported for output/input */
|
||||||
|
|
||||||
|
typedef uint32_t BMDPixelFormat;
|
||||||
|
enum _BMDPixelFormat {
|
||||||
|
bmdFormatUnspecified = 0,
|
||||||
|
bmdFormat8BitYUV = /* '2vuy' */ 0x32767579,
|
||||||
|
bmdFormat10BitYUV = /* 'v210' */ 0x76323130,
|
||||||
|
bmdFormat10BitYUVA = /* 'Ay10' */ 0x41793130, // Big-endian YUVA 10 bit per component with SMPTE video levels (64-940) for YUV but full range alpha
|
||||||
|
bmdFormat8BitARGB = 32,
|
||||||
|
bmdFormat8BitBGRA = /* 'BGRA' */ 0x42475241,
|
||||||
|
bmdFormat10BitRGB = /* 'r210' */ 0x72323130, // Big-endian RGB 10-bit per component with SMPTE video levels (64-940). Packed as 2:10:10:10
|
||||||
|
bmdFormat12BitRGB = /* 'R12B' */ 0x52313242, // Big-endian RGB 12-bit per component with full range (0-4095). Packed as 12-bit per component
|
||||||
|
bmdFormat12BitRGBLE = /* 'R12L' */ 0x5231324C, // Little-endian RGB 12-bit per component with full range (0-4095). Packed as 12-bit per component
|
||||||
|
bmdFormat10BitRGBXLE = /* 'R10l' */ 0x5231306C, // Little-endian 10-bit RGB with SMPTE video levels (64-940)
|
||||||
|
bmdFormat10BitRGBX = /* 'R10b' */ 0x52313062, // Big-endian 10-bit RGB with SMPTE video levels (64-940)
|
||||||
|
|
||||||
|
/* Formats supported only by devices that can be queried for an IDeckLinkEncoderInput */
|
||||||
|
|
||||||
|
bmdFormatH265 = /* 'hev1' */ 0x68657631,
|
||||||
|
bmdFormatDNxHR = /* 'AVdh' */ 0x41566468
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Enum BMDDisplayModeFlags - Flags to describe the characteristics of an IDeckLinkDisplayMode. */
|
||||||
|
|
||||||
|
typedef uint32_t BMDDisplayModeFlags;
|
||||||
|
enum _BMDDisplayModeFlags {
|
||||||
|
bmdDisplayModeSupports3D = 1 << 0,
|
||||||
|
bmdDisplayModeColorspaceRec601 = 1 << 1,
|
||||||
|
bmdDisplayModeColorspaceRec709 = 1 << 2,
|
||||||
|
bmdDisplayModeColorspaceRec2020 = 1 << 3
|
||||||
|
};
|
||||||
|
|
||||||
|
#if defined(__cplusplus)
|
||||||
|
|
||||||
|
// Forward Declarations
|
||||||
|
|
||||||
|
class IDeckLinkDisplayModeIterator;
|
||||||
|
class IDeckLinkDisplayMode;
|
||||||
|
|
||||||
|
/* Interface IDeckLinkDisplayModeIterator - Enumerates over supported input/output display modes. */
|
||||||
|
|
||||||
|
class BMD_PUBLIC IDeckLinkDisplayModeIterator : public IUnknown
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual HRESULT Next (/* out */ IDeckLinkDisplayMode** deckLinkDisplayMode) = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual ~IDeckLinkDisplayModeIterator () {} // call Release method to drop reference count
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Interface IDeckLinkDisplayMode - Represents a display mode */
|
||||||
|
|
||||||
|
class BMD_PUBLIC IDeckLinkDisplayMode : public IUnknown
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual HRESULT GetName (/* out */ const char** name) = 0;
|
||||||
|
virtual BMDDisplayMode GetDisplayMode (void) = 0;
|
||||||
|
virtual long GetWidth (void) = 0;
|
||||||
|
virtual long GetHeight (void) = 0;
|
||||||
|
virtual HRESULT GetFrameRate (/* out */ BMDTimeValue* frameDuration, /* out */ BMDTimeScale* timeScale) = 0;
|
||||||
|
virtual BMDFieldDominance GetFieldDominance (void) = 0;
|
||||||
|
virtual BMDDisplayModeFlags GetFlags (void) = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual ~IDeckLinkDisplayMode () {} // call Release method to drop reference count
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Functions */
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#endif /* defined(__cplusplus) */
|
||||||
|
#endif /* defined(BMD_DECKLINKAPIMODES_H) */
|
||||||
|
|
@ -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) */
|
||||||
140
services/capture/sdk/DeckLinkAPITypes.h
Normal file
140
services/capture/sdk/DeckLinkAPITypes.h
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
/* -LICENSE-START-
|
||||||
|
** Copyright (c) 2026 Blackmagic Design
|
||||||
|
**
|
||||||
|
** Permission is hereby granted, free of charge, to any person or organization
|
||||||
|
** obtaining a copy of the software and accompanying documentation covered by
|
||||||
|
** this license (the "Software") to use, reproduce, display, distribute,
|
||||||
|
** execute, and transmit the Software, and to prepare derivative works of the
|
||||||
|
** Software, and to permit third-parties to whom the Software is furnished to
|
||||||
|
** do so, all subject to the following:
|
||||||
|
**
|
||||||
|
** The copyright notices in the Software and this entire statement, including
|
||||||
|
** the above license grant, this restriction and the following disclaimer,
|
||||||
|
** must be included in all copies of the Software, in whole or in part, and
|
||||||
|
** all derivative works of the Software, unless such copies or derivative
|
||||||
|
** works are solely in the form of machine-executable object code generated by
|
||||||
|
** a source language processor.
|
||||||
|
**
|
||||||
|
** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||||
|
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||||
|
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||||
|
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
** DEALINGS IN THE SOFTWARE.
|
||||||
|
** -LICENSE-END-
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -- AUTOMATICALLY GENERATED - DO NOT EDIT ---
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef BMD_DECKLINKAPITYPES_H
|
||||||
|
#define BMD_DECKLINKAPITYPES_H
|
||||||
|
|
||||||
|
|
||||||
|
#ifndef BMD_CONST
|
||||||
|
#if defined(_MSC_VER)
|
||||||
|
#define BMD_CONST __declspec(selectany) static const
|
||||||
|
#else
|
||||||
|
#define BMD_CONST static const
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef BMD_PUBLIC
|
||||||
|
#define BMD_PUBLIC
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Type Declarations
|
||||||
|
|
||||||
|
typedef int64_t BMDTimeValue;
|
||||||
|
typedef int64_t BMDTimeScale;
|
||||||
|
typedef uint32_t BMDTimecodeBCD;
|
||||||
|
typedef uint32_t BMDTimecodeUserBits;
|
||||||
|
typedef int64_t BMDIPFlowID;
|
||||||
|
|
||||||
|
// Interface ID Declarations
|
||||||
|
|
||||||
|
BMD_CONST REFIID IID_IDeckLinkTimecode = /* BC6CFBD3-8317-4325-AC1C-1216391E9340 */ { 0xBC,0x6C,0xFB,0xD3,0x83,0x17,0x43,0x25,0xAC,0x1C,0x12,0x16,0x39,0x1E,0x93,0x40 };
|
||||||
|
|
||||||
|
/* Enum BMDTimecodeFlags - Timecode flags */
|
||||||
|
|
||||||
|
typedef uint32_t BMDTimecodeFlags;
|
||||||
|
enum _BMDTimecodeFlags {
|
||||||
|
bmdTimecodeFlagDefault = 0,
|
||||||
|
bmdTimecodeIsDropFrame = 1 << 0,
|
||||||
|
bmdTimecodeFieldMark = 1 << 1,
|
||||||
|
bmdTimecodeColorFrame = 1 << 2,
|
||||||
|
bmdTimecodeEmbedRecordingTrigger = 1 << 3, // On SDI recording trigger utilises a user-bit.
|
||||||
|
bmdTimecodeRecordingTriggered = 1 << 4
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Enum BMDVideoConnection - Video connection types */
|
||||||
|
|
||||||
|
typedef uint32_t BMDVideoConnection;
|
||||||
|
enum _BMDVideoConnection {
|
||||||
|
bmdVideoConnectionUnspecified = 0,
|
||||||
|
bmdVideoConnectionSDI = 1 << 0,
|
||||||
|
bmdVideoConnectionHDMI = 1 << 1,
|
||||||
|
bmdVideoConnectionOpticalSDI = 1 << 2,
|
||||||
|
bmdVideoConnectionComponent = 1 << 3,
|
||||||
|
bmdVideoConnectionComposite = 1 << 4,
|
||||||
|
bmdVideoConnectionSVideo = 1 << 5,
|
||||||
|
bmdVideoConnectionEthernet = 1 << 6,
|
||||||
|
bmdVideoConnectionOpticalEthernet = 1 << 7,
|
||||||
|
bmdVideoConnectionInternal = 1 << 8
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Enum BMDAudioConnection - Audio connection types */
|
||||||
|
|
||||||
|
typedef uint32_t BMDAudioConnection;
|
||||||
|
enum _BMDAudioConnection {
|
||||||
|
bmdAudioConnectionEmbedded = 1 << 0,
|
||||||
|
bmdAudioConnectionAESEBU = 1 << 1,
|
||||||
|
bmdAudioConnectionAnalog = 1 << 2,
|
||||||
|
bmdAudioConnectionAnalogXLR = 1 << 3,
|
||||||
|
bmdAudioConnectionAnalogRCA = 1 << 4,
|
||||||
|
bmdAudioConnectionMicrophone = 1 << 5,
|
||||||
|
bmdAudioConnectionHeadphones = 1 << 6
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Enum BMDDeckControlConnection - Deck control connections */
|
||||||
|
|
||||||
|
typedef uint32_t BMDDeckControlConnection;
|
||||||
|
enum _BMDDeckControlConnection {
|
||||||
|
bmdDeckControlConnectionRS422Remote1 = 1 << 0,
|
||||||
|
bmdDeckControlConnectionRS422Remote2 = 1 << 1
|
||||||
|
};
|
||||||
|
|
||||||
|
#if defined(__cplusplus)
|
||||||
|
|
||||||
|
// Forward Declarations
|
||||||
|
|
||||||
|
class IDeckLinkTimecode;
|
||||||
|
|
||||||
|
/* Interface IDeckLinkTimecode - Used for video frame timecode representation. */
|
||||||
|
|
||||||
|
class BMD_PUBLIC IDeckLinkTimecode : public IUnknown
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual BMDTimecodeBCD GetBCD (void) = 0;
|
||||||
|
virtual HRESULT GetComponents (/* out */ uint8_t* hours, /* out */ uint8_t* minutes, /* out */ uint8_t* seconds, /* out */ uint8_t* frames) = 0;
|
||||||
|
virtual HRESULT GetString (/* out */ const char** timecode) = 0;
|
||||||
|
virtual BMDTimecodeFlags GetFlags (void) = 0;
|
||||||
|
virtual HRESULT GetTimecodeUserBits (/* out */ BMDTimecodeUserBits* userBits) = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual ~IDeckLinkTimecode () {} // call Release method to drop reference count
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Functions */
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#endif /* defined(__cplusplus) */
|
||||||
|
#endif /* defined(BMD_DECKLINKAPITYPES_H) */
|
||||||
50
services/capture/sdk/DeckLinkAPIVersion.h
Normal file
50
services/capture/sdk/DeckLinkAPIVersion.h
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
/* -LICENSE-START-
|
||||||
|
* ** Copyright (c) 2014 Blackmagic Design
|
||||||
|
* **
|
||||||
|
* ** Permission is hereby granted, free of charge, to any person or organization
|
||||||
|
* ** obtaining a copy of the software and accompanying documentation (the
|
||||||
|
* ** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||||
|
* ** and transmit the Software, and to prepare derivative works of the Software,
|
||||||
|
* ** and to permit third-parties to whom the Software is furnished to do so, in
|
||||||
|
* ** accordance with:
|
||||||
|
* **
|
||||||
|
* ** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||||
|
* ** Agreement for the Software Development Kit ("EULA") available at
|
||||||
|
* ** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||||
|
* **
|
||||||
|
* ** (2) if the Software is obtained from any third party, such licensing terms
|
||||||
|
* ** as notified by that third party,
|
||||||
|
* **
|
||||||
|
* ** and all subject to the following:
|
||||||
|
* **
|
||||||
|
* ** (3) the copyright notices in the Software and this entire statement,
|
||||||
|
* ** including the above license grant, this restriction and the following
|
||||||
|
* ** disclaimer, must be included in all copies of the Software, in whole or in
|
||||||
|
* ** part, and all derivative works of the Software, unless such copies or
|
||||||
|
* ** derivative works are solely in the form of machine-executable object code
|
||||||
|
* ** generated by a source language processor.
|
||||||
|
* **
|
||||||
|
* ** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
* ** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* ** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||||
|
* ** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||||
|
* ** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||||
|
* ** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
* ** DEALINGS IN THE SOFTWARE.
|
||||||
|
* **
|
||||||
|
* ** A copy of the Software is available free of charge at
|
||||||
|
* ** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||||
|
* **
|
||||||
|
* ** -LICENSE-END-
|
||||||
|
* */
|
||||||
|
|
||||||
|
/* DeckLinkAPIVersion.h */
|
||||||
|
|
||||||
|
#ifndef __DeckLink_API_Version_h__
|
||||||
|
#define __DeckLink_API_Version_h__
|
||||||
|
|
||||||
|
#define BLACKMAGIC_DECKLINK_API_VERSION 0x10000000
|
||||||
|
#define BLACKMAGIC_DECKLINK_API_VERSION_STRING "16.0"
|
||||||
|
|
||||||
|
#endif // __DeckLink_API_Version_h__
|
||||||
|
|
||||||
62
services/capture/sdk/DeckLinkAPIVideoConversion_v14_2_1.h
Normal file
62
services/capture/sdk/DeckLinkAPIVideoConversion_v14_2_1.h
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
/* -LICENSE-START-
|
||||||
|
** Copyright (c) 2022 Blackmagic Design
|
||||||
|
**
|
||||||
|
** Permission is hereby granted, free of charge, to any person or organization
|
||||||
|
** obtaining a copy of the software and accompanying documentation (the
|
||||||
|
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||||
|
** and transmit the Software, and to prepare derivative works of the Software,
|
||||||
|
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||||
|
** accordance with:
|
||||||
|
**
|
||||||
|
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||||
|
** Agreement for the Software Development Kit (“EULA”) available at
|
||||||
|
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||||
|
**
|
||||||
|
** (2) if the Software is obtained from any third party, such licensing terms
|
||||||
|
** as notified by that third party,
|
||||||
|
**
|
||||||
|
** and all subject to the following:
|
||||||
|
**
|
||||||
|
** (3) the copyright notices in the Software and this entire statement,
|
||||||
|
** including the above license grant, this restriction and the following
|
||||||
|
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||||
|
** part, and all derivative works of the Software, unless such copies or
|
||||||
|
** derivative works are solely in the form of machine-executable object code
|
||||||
|
** generated by a source language processor.
|
||||||
|
**
|
||||||
|
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||||
|
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||||
|
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||||
|
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
** DEALINGS IN THE SOFTWARE.
|
||||||
|
**
|
||||||
|
** A copy of the Software is available free of charge at
|
||||||
|
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||||
|
**
|
||||||
|
** -LICENSE-END-
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef BMD_DECKLINKAPIVIDEOCONVERSION_v14_2_1_H
|
||||||
|
#define BMD_DECKLINKAPIVIDEOCONVERSION_v14_2_1_H
|
||||||
|
|
||||||
|
#include "DeckLinkAPI.h"
|
||||||
|
#include "DeckLinkAPIVideoFrame_v14_2_1.h"
|
||||||
|
|
||||||
|
// Type Declarations
|
||||||
|
|
||||||
|
BMD_CONST REFIID IID_IDeckLinkVideoConversion_v14_2_1 = /* 3BBCB8A2-DA2C-42D9-B5D8-88083644E99A */ { 0x3B, 0xBC, 0xB8, 0xA2, 0xDA, 0x2C, 0x42, 0xD9, 0xB5, 0xD8, 0x88, 0x08, 0x36, 0x44, 0xE9, 0x9A };
|
||||||
|
|
||||||
|
/* Interface IDeckLinkVideoConversion - Created with CoCreateInstance. */
|
||||||
|
|
||||||
|
class BMD_PUBLIC IDeckLinkVideoConversion_v14_2_1 : public IUnknown
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual HRESULT ConvertFrame (/* in */ IDeckLinkVideoFrame_v14_2_1* srcFrame, /* in */ IDeckLinkVideoFrame_v14_2_1* dstFrame) = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual ~IDeckLinkVideoConversion_v14_2_1 () {} // call Release method to drop reference count
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
88
services/capture/sdk/DeckLinkAPIVideoEncoderInput_v10_11.h
Normal file
88
services/capture/sdk/DeckLinkAPIVideoEncoderInput_v10_11.h
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
/* -LICENSE-START-
|
||||||
|
** Copyright (c) 2017 Blackmagic Design
|
||||||
|
**
|
||||||
|
** Permission is hereby granted, free of charge, to any person or organization
|
||||||
|
** obtaining a copy of the software and accompanying documentation (the
|
||||||
|
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||||
|
** and transmit the Software, and to prepare derivative works of the Software,
|
||||||
|
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||||
|
** accordance with:
|
||||||
|
**
|
||||||
|
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||||
|
** Agreement for the Software Development Kit ("EULA") available at
|
||||||
|
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||||
|
**
|
||||||
|
** (2) if the Software is obtained from any third party, such licensing terms
|
||||||
|
** as notified by that third party,
|
||||||
|
**
|
||||||
|
** and all subject to the following:
|
||||||
|
**
|
||||||
|
** (3) the copyright notices in the Software and this entire statement,
|
||||||
|
** including the above license grant, this restriction and the following
|
||||||
|
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||||
|
** part, and all derivative works of the Software, unless such copies or
|
||||||
|
** derivative works are solely in the form of machine-executable object code
|
||||||
|
** generated by a source language processor.
|
||||||
|
**
|
||||||
|
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||||
|
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||||
|
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||||
|
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
** DEALINGS IN THE SOFTWARE.
|
||||||
|
**
|
||||||
|
** A copy of the Software is available free of charge at
|
||||||
|
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||||
|
**
|
||||||
|
** -LICENSE-END-
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef BMD_DECKLINKAPIVIDEOENCODERINPUT_v10_11_H
|
||||||
|
#define BMD_DECKLINKAPIVIDEOENCODERINPUT_v10_11_H
|
||||||
|
|
||||||
|
#include "DeckLinkAPI.h"
|
||||||
|
#include "DeckLinkAPI_v10_11.h"
|
||||||
|
#include "DeckLinkAPIMemoryAllocator_v14_2_1.h"
|
||||||
|
|
||||||
|
// Type Declarations
|
||||||
|
BMD_CONST REFIID IID_IDeckLinkEncoderInput_v10_11 = /* 270587DA-6B7D-42E7-A1F0-6D853F581185 */ {0x27,0x05,0x87,0xDA,0x6B,0x7D,0x42,0xE7,0xA1,0xF0,0x6D,0x85,0x3F,0x58,0x11,0x85};
|
||||||
|
|
||||||
|
/* Interface IDeckLinkEncoderInput_v10_11 - Created by QueryInterface from IDeckLink. */
|
||||||
|
|
||||||
|
class IDeckLinkEncoderInput_v10_11 : public IUnknown
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual HRESULT DoesSupportVideoMode (/* in */ BMDDisplayMode displayMode, /* in */ BMDPixelFormat pixelFormat, /* in */ BMDVideoInputFlags flags, /* out */ BMDDisplayModeSupport_v10_11 *result, /* out */ IDeckLinkDisplayMode **resultDisplayMode) = 0;
|
||||||
|
virtual HRESULT GetDisplayModeIterator (/* out */ IDeckLinkDisplayModeIterator **iterator) = 0;
|
||||||
|
|
||||||
|
/* Video Input */
|
||||||
|
|
||||||
|
virtual HRESULT EnableVideoInput (/* in */ BMDDisplayMode displayMode, /* in */ BMDPixelFormat pixelFormat, /* in */ BMDVideoInputFlags flags) = 0;
|
||||||
|
virtual HRESULT DisableVideoInput (void) = 0;
|
||||||
|
virtual HRESULT GetAvailablePacketsCount (/* out */ uint32_t *availablePacketsCount) = 0;
|
||||||
|
virtual HRESULT SetMemoryAllocator (/* in */ IDeckLinkMemoryAllocator_v14_2_1 *theAllocator) = 0;
|
||||||
|
|
||||||
|
/* Audio Input */
|
||||||
|
|
||||||
|
virtual HRESULT EnableAudioInput (/* in */ BMDAudioFormat audioFormat, /* in */ BMDAudioSampleRate sampleRate, /* in */ BMDAudioSampleType sampleType, /* in */ uint32_t channelCount) = 0;
|
||||||
|
virtual HRESULT DisableAudioInput (void) = 0;
|
||||||
|
virtual HRESULT GetAvailableAudioSampleFrameCount (/* out */ uint32_t *availableSampleFrameCount) = 0;
|
||||||
|
|
||||||
|
/* Input Control */
|
||||||
|
|
||||||
|
virtual HRESULT StartStreams (void) = 0;
|
||||||
|
virtual HRESULT StopStreams (void) = 0;
|
||||||
|
virtual HRESULT PauseStreams (void) = 0;
|
||||||
|
virtual HRESULT FlushStreams (void) = 0;
|
||||||
|
virtual HRESULT SetCallback (/* in */ IDeckLinkEncoderInputCallback *theCallback) = 0;
|
||||||
|
|
||||||
|
/* Hardware Timing */
|
||||||
|
|
||||||
|
virtual HRESULT GetHardwareReferenceClock (/* in */ BMDTimeScale desiredTimeScale, /* out */ BMDTimeValue *hardwareTime, /* out */ BMDTimeValue *timeInFrame, /* out */ BMDTimeValue *ticksPerFrame) = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual ~IDeckLinkEncoderInput_v10_11 () {} // call Release method to drop reference count
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /* defined(BMD_DECKLINKAPIVIDEOENCODERINPUT_v10_11_H) */
|
||||||
|
|
@ -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) */
|
||||||
68
services/capture/sdk/DeckLinkAPIVideoFrame_v14_2_1.h
Normal file
68
services/capture/sdk/DeckLinkAPIVideoFrame_v14_2_1.h
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
/* -LICENSE-START-
|
||||||
|
** Copyright (c) 2022 Blackmagic Design
|
||||||
|
**
|
||||||
|
** Permission is hereby granted, free of charge, to any person or organization
|
||||||
|
** obtaining a copy of the software and accompanying documentation (the
|
||||||
|
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||||
|
** and transmit the Software, and to prepare derivative works of the Software,
|
||||||
|
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||||
|
** accordance with:
|
||||||
|
**
|
||||||
|
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||||
|
** Agreement for the Software Development Kit (“EULA”) available at
|
||||||
|
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||||
|
**
|
||||||
|
** (2) if the Software is obtained from any third party, such licensing terms
|
||||||
|
** as notified by that third party,
|
||||||
|
**
|
||||||
|
** and all subject to the following:
|
||||||
|
**
|
||||||
|
** (3) the copyright notices in the Software and this entire statement,
|
||||||
|
** including the above license grant, this restriction and the following
|
||||||
|
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||||
|
** part, and all derivative works of the Software, unless such copies or
|
||||||
|
** derivative works are solely in the form of machine-executable object code
|
||||||
|
** generated by a source language processor.
|
||||||
|
**
|
||||||
|
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||||
|
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||||
|
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||||
|
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
** DEALINGS IN THE SOFTWARE.
|
||||||
|
**
|
||||||
|
** A copy of the Software is available free of charge at
|
||||||
|
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||||
|
**
|
||||||
|
** -LICENSE-END-
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef BMD_DECKLINKAPIVIDEOFRAME_v14_2_1_H
|
||||||
|
#define BMD_DECKLINKAPIVIDEOFRAME_v14_2_1_H
|
||||||
|
|
||||||
|
#include "DeckLinkAPI.h"
|
||||||
|
|
||||||
|
// Type Declarations
|
||||||
|
|
||||||
|
BMD_CONST REFIID IID_IDeckLinkVideoFrame_v14_2_1 = /* 3F716FE0-F023-4111-BE5D-EF4414C05B17 */ { 0x3F, 0x71, 0x6F, 0xE0, 0xF0, 0x23, 0x41, 0x11, 0xBE, 0x5D, 0xEF, 0x44, 0x14, 0xC0, 0x5B, 0x17 };
|
||||||
|
|
||||||
|
/* Interface IDeckLinkVideoFrame - Interface to encapsulate a video frame; can be caller-implemented. */
|
||||||
|
|
||||||
|
class BMD_PUBLIC IDeckLinkVideoFrame_v14_2_1 : public IUnknown
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual long GetWidth (void) = 0;
|
||||||
|
virtual long GetHeight (void) = 0;
|
||||||
|
virtual long GetRowBytes (void) = 0;
|
||||||
|
virtual BMDPixelFormat GetPixelFormat (void) = 0;
|
||||||
|
virtual BMDFrameFlags GetFlags (void) = 0;
|
||||||
|
virtual HRESULT GetBytes (/* out */ void** buffer) = 0;
|
||||||
|
virtual HRESULT GetTimecode (/* in */ BMDTimecodeFormat format, /* out */ IDeckLinkTimecode** timecode) = 0;
|
||||||
|
virtual HRESULT GetAncillaryData (/* out */ IDeckLinkVideoFrameAncillary** ancillary) = 0; // Use of IDeckLinkVideoFrameAncillaryPackets is preferred
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual ~IDeckLinkVideoFrame_v14_2_1 () {} // call Release method to drop reference count
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /* defined(BMD_DECKLINKAPIVIDEOFRAME_v14_2_1_H) */
|
||||||
91
services/capture/sdk/DeckLinkAPIVideoInput_v10_11.h
Normal file
91
services/capture/sdk/DeckLinkAPIVideoInput_v10_11.h
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
/* -LICENSE-START-
|
||||||
|
** Copyright (c) 2017 Blackmagic Design
|
||||||
|
**
|
||||||
|
** Permission is hereby granted, free of charge, to any person or organization
|
||||||
|
** obtaining a copy of the software and accompanying documentation (the
|
||||||
|
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||||
|
** and transmit the Software, and to prepare derivative works of the Software,
|
||||||
|
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||||
|
** accordance with:
|
||||||
|
**
|
||||||
|
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||||
|
** Agreement for the Software Development Kit ("EULA") available at
|
||||||
|
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||||
|
**
|
||||||
|
** (2) if the Software is obtained from any third party, such licensing terms
|
||||||
|
** as notified by that third party,
|
||||||
|
**
|
||||||
|
** and all subject to the following:
|
||||||
|
**
|
||||||
|
** (3) the copyright notices in the Software and this entire statement,
|
||||||
|
** including the above license grant, this restriction and the following
|
||||||
|
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||||
|
** part, and all derivative works of the Software, unless such copies or
|
||||||
|
** derivative works are solely in the form of machine-executable object code
|
||||||
|
** generated by a source language processor.
|
||||||
|
**
|
||||||
|
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||||
|
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||||
|
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||||
|
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
** DEALINGS IN THE SOFTWARE.
|
||||||
|
**
|
||||||
|
** A copy of the Software is available free of charge at
|
||||||
|
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||||
|
**
|
||||||
|
** -LICENSE-END-
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef BMD_DECKLINKAPIVIDEOINPUT_v10_11_H
|
||||||
|
#define BMD_DECKLINKAPIVIDEOINPUT_v10_11_H
|
||||||
|
|
||||||
|
#include "DeckLinkAPI.h"
|
||||||
|
#include "DeckLinkAPI_v10_11.h"
|
||||||
|
#include "DeckLinkAPIMemoryAllocator_v14_2_1.h"
|
||||||
|
#include "DeckLinkAPIVideoInput_v11_5_1.h"
|
||||||
|
|
||||||
|
// Type Declarations
|
||||||
|
BMD_CONST REFIID IID_IDeckLinkInput_v10_11 = /* AF22762B-DFAC-4846-AA79-FA8883560995 */ {0xAF,0x22,0x76,0x2B,0xDF,0xAC,0x48,0x46,0xAA,0x79,0xFA,0x88,0x83,0x56,0x09,0x95};
|
||||||
|
|
||||||
|
/* Interface IDeckLinkInput_v10_11 - DeckLink input interface. */
|
||||||
|
|
||||||
|
class IDeckLinkInput_v10_11 : public IUnknown
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual HRESULT DoesSupportVideoMode (/* in */ BMDDisplayMode displayMode, /* in */ BMDPixelFormat pixelFormat, /* in */ BMDVideoInputFlags flags, /* out */ BMDDisplayModeSupport_v10_11 *result, /* out */ IDeckLinkDisplayMode **resultDisplayMode) = 0;
|
||||||
|
virtual HRESULT GetDisplayModeIterator (/* out */ IDeckLinkDisplayModeIterator **iterator) = 0;
|
||||||
|
|
||||||
|
virtual HRESULT SetScreenPreviewCallback (/* in */ IDeckLinkScreenPreviewCallback_v14_2_1 *previewCallback) = 0;
|
||||||
|
|
||||||
|
/* Video Input */
|
||||||
|
|
||||||
|
virtual HRESULT EnableVideoInput (/* in */ BMDDisplayMode displayMode, /* in */ BMDPixelFormat pixelFormat, /* in */ BMDVideoInputFlags flags) = 0;
|
||||||
|
virtual HRESULT DisableVideoInput (void) = 0;
|
||||||
|
virtual HRESULT GetAvailableVideoFrameCount (/* out */ uint32_t *availableFrameCount) = 0;
|
||||||
|
virtual HRESULT SetVideoInputFrameMemoryAllocator (/* in */ IDeckLinkMemoryAllocator_v14_2_1 *theAllocator) = 0;
|
||||||
|
|
||||||
|
/* Audio Input */
|
||||||
|
|
||||||
|
virtual HRESULT EnableAudioInput (/* in */ BMDAudioSampleRate sampleRate, /* in */ BMDAudioSampleType sampleType, /* in */ uint32_t channelCount) = 0;
|
||||||
|
virtual HRESULT DisableAudioInput (void) = 0;
|
||||||
|
virtual HRESULT GetAvailableAudioSampleFrameCount (/* out */ uint32_t *availableSampleFrameCount) = 0;
|
||||||
|
|
||||||
|
/* Input Control */
|
||||||
|
|
||||||
|
virtual HRESULT StartStreams (void) = 0;
|
||||||
|
virtual HRESULT StopStreams (void) = 0;
|
||||||
|
virtual HRESULT PauseStreams (void) = 0;
|
||||||
|
virtual HRESULT FlushStreams (void) = 0;
|
||||||
|
virtual HRESULT SetCallback (/* in */ IDeckLinkInputCallback_v11_5_1 *theCallback) = 0;
|
||||||
|
|
||||||
|
/* Hardware Timing */
|
||||||
|
|
||||||
|
virtual HRESULT GetHardwareReferenceClock (/* in */ BMDTimeScale desiredTimeScale, /* out */ BMDTimeValue *hardwareTime, /* out */ BMDTimeValue *timeInFrame, /* out */ BMDTimeValue *ticksPerFrame) = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual ~IDeckLinkInput_v10_11 () {} // call Release method to drop reference count
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /* defined(BMD_DECKLINKAPIVIDEOINPUT_v10_11_H) */
|
||||||
90
services/capture/sdk/DeckLinkAPIVideoInput_v11_4.h
Normal file
90
services/capture/sdk/DeckLinkAPIVideoInput_v11_4.h
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
/* -LICENSE-START-
|
||||||
|
** Copyright (c) 2019 Blackmagic Design
|
||||||
|
**
|
||||||
|
** Permission is hereby granted, free of charge, to any person or organization
|
||||||
|
** obtaining a copy of the software and accompanying documentation (the
|
||||||
|
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||||
|
** and transmit the Software, and to prepare derivative works of the Software,
|
||||||
|
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||||
|
** accordance with:
|
||||||
|
**
|
||||||
|
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||||
|
** Agreement for the Software Development Kit ("EULA") available at
|
||||||
|
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||||
|
**
|
||||||
|
** (2) if the Software is obtained from any third party, such licensing terms
|
||||||
|
** as notified by that third party,
|
||||||
|
**
|
||||||
|
** and all subject to the following:
|
||||||
|
**
|
||||||
|
** (3) the copyright notices in the Software and this entire statement,
|
||||||
|
** including the above license grant, this restriction and the following
|
||||||
|
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||||
|
** part, and all derivative works of the Software, unless such copies or
|
||||||
|
** derivative works are solely in the form of machine-executable object code
|
||||||
|
** generated by a source language processor.
|
||||||
|
**
|
||||||
|
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||||
|
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||||
|
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||||
|
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
** DEALINGS IN THE SOFTWARE.
|
||||||
|
**
|
||||||
|
** A copy of the Software is available free of charge at
|
||||||
|
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||||
|
**
|
||||||
|
** -LICENSE-END-
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef BMD_DECKLINKAPIVIDEOINPUT_v11_4_H
|
||||||
|
#define BMD_DECKLINKAPIVIDEOINPUT_v11_4_H
|
||||||
|
|
||||||
|
#include "DeckLinkAPI.h"
|
||||||
|
#include "DeckLinkAPIMemoryAllocator_v14_2_1.h"
|
||||||
|
#include "DeckLinkAPIVideoInput_v11_5_1.h"
|
||||||
|
|
||||||
|
// Type Declarations
|
||||||
|
BMD_CONST REFIID IID_IDeckLinkInput_v11_4 = /* 2A88CF76-F494-4216-A7EF-DC74EEB83882 */ { 0x2A,0x88,0xCF,0x76,0xF4,0x94,0x42,0x16,0xA7,0xEF,0xDC,0x74,0xEE,0xB8,0x38,0x82 };
|
||||||
|
|
||||||
|
/* Interface IDeckLinkInput_v11_4 - Created by QueryInterface from IDeckLink. */
|
||||||
|
|
||||||
|
class BMD_PUBLIC IDeckLinkInput_v11_4 : public IUnknown
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual HRESULT DoesSupportVideoMode (/* in */ BMDVideoConnection connection /* If a value of 0 is specified, the caller does not care about the connection */, /* in */ BMDDisplayMode requestedMode, /* in */ BMDPixelFormat requestedPixelFormat, /* in */ BMDSupportedVideoModeFlags flags, /* out */ bool* supported) = 0;
|
||||||
|
virtual HRESULT GetDisplayMode (/* in */ BMDDisplayMode displayMode, /* out */ IDeckLinkDisplayMode** resultDisplayMode) = 0;
|
||||||
|
virtual HRESULT GetDisplayModeIterator (/* out */ IDeckLinkDisplayModeIterator** iterator) = 0;
|
||||||
|
virtual HRESULT SetScreenPreviewCallback (/* in */ IDeckLinkScreenPreviewCallback_v14_2_1* previewCallback) = 0;
|
||||||
|
|
||||||
|
/* Video Input */
|
||||||
|
|
||||||
|
virtual HRESULT EnableVideoInput (/* in */ BMDDisplayMode displayMode, /* in */ BMDPixelFormat pixelFormat, /* in */ BMDVideoInputFlags flags) = 0;
|
||||||
|
virtual HRESULT DisableVideoInput (void) = 0;
|
||||||
|
virtual HRESULT GetAvailableVideoFrameCount (/* out */ uint32_t* availableFrameCount) = 0;
|
||||||
|
virtual HRESULT SetVideoInputFrameMemoryAllocator (/* in */ IDeckLinkMemoryAllocator_v14_2_1* theAllocator) = 0;
|
||||||
|
|
||||||
|
/* Audio Input */
|
||||||
|
|
||||||
|
virtual HRESULT EnableAudioInput (/* in */ BMDAudioSampleRate sampleRate, /* in */ BMDAudioSampleType sampleType, /* in */ uint32_t channelCount) = 0;
|
||||||
|
virtual HRESULT DisableAudioInput (void) = 0;
|
||||||
|
virtual HRESULT GetAvailableAudioSampleFrameCount (/* out */ uint32_t* availableSampleFrameCount) = 0;
|
||||||
|
|
||||||
|
/* Input Control */
|
||||||
|
|
||||||
|
virtual HRESULT StartStreams (void) = 0;
|
||||||
|
virtual HRESULT StopStreams (void) = 0;
|
||||||
|
virtual HRESULT PauseStreams (void) = 0;
|
||||||
|
virtual HRESULT FlushStreams (void) = 0;
|
||||||
|
virtual HRESULT SetCallback (/* in */ IDeckLinkInputCallback_v11_5_1* theCallback) = 0;
|
||||||
|
|
||||||
|
/* Hardware Timing */
|
||||||
|
|
||||||
|
virtual HRESULT GetHardwareReferenceClock (/* in */ BMDTimeScale desiredTimeScale, /* out */ BMDTimeValue* hardwareTime, /* out */ BMDTimeValue* timeInFrame, /* out */ BMDTimeValue* ticksPerFrame) = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual ~IDeckLinkInput_v11_4 () {} // call Release method to drop reference count
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /* defined(BMD_DECKLINKAPIVIDEOINPUT_v11_4_H) */
|
||||||
103
services/capture/sdk/DeckLinkAPIVideoInput_v11_5_1.h
Normal file
103
services/capture/sdk/DeckLinkAPIVideoInput_v11_5_1.h
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
/* -LICENSE-START-
|
||||||
|
** Copyright (c) 2020 Blackmagic Design
|
||||||
|
**
|
||||||
|
** Permission is hereby granted, free of charge, to any person or organization
|
||||||
|
** obtaining a copy of the software and accompanying documentation (the
|
||||||
|
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||||
|
** and transmit the Software, and to prepare derivative works of the Software,
|
||||||
|
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||||
|
** accordance with:
|
||||||
|
**
|
||||||
|
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||||
|
** Agreement for the Software Development Kit ("EULA") available at
|
||||||
|
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||||
|
**
|
||||||
|
** (2) if the Software is obtained from any third party, such licensing terms
|
||||||
|
** as notified by that third party,
|
||||||
|
**
|
||||||
|
** and all subject to the following:
|
||||||
|
**
|
||||||
|
** (3) the copyright notices in the Software and this entire statement,
|
||||||
|
** including the above license grant, this restriction and the following
|
||||||
|
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||||
|
** part, and all derivative works of the Software, unless such copies or
|
||||||
|
** derivative works are solely in the form of machine-executable object code
|
||||||
|
** generated by a source language processor.
|
||||||
|
**
|
||||||
|
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||||
|
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||||
|
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||||
|
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
** DEALINGS IN THE SOFTWARE.
|
||||||
|
**
|
||||||
|
** A copy of the Software is available free of charge at
|
||||||
|
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||||
|
**
|
||||||
|
** -LICENSE-END-
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef BMD_DECKLINKAPIVIDEOINPUT_v11_5_1_H
|
||||||
|
#define BMD_DECKLINKAPIVIDEOINPUT_v11_5_1_H
|
||||||
|
|
||||||
|
#include "DeckLinkAPI.h"
|
||||||
|
#include "DeckLinkAPIVideoInput_v14_2_1.h"
|
||||||
|
|
||||||
|
// Type Declarations
|
||||||
|
|
||||||
|
BMD_CONST REFIID IID_IDeckLinkInputCallback_v11_5_1 = /* DD04E5EC-7415-42AB-AE4A-E80C4DFC044A */ { 0xDD, 0x04, 0xE5, 0xEC, 0x74, 0x15, 0x42, 0xAB, 0xAE, 0x4A, 0xE8, 0x0C, 0x4D, 0xFC, 0x04, 0x4A };
|
||||||
|
BMD_CONST REFIID IID_IDeckLinkInput_v11_5_1 = /* 9434C6E4-B15D-4B1C-979E-661E3DDCB4B9 */ { 0x94, 0x34, 0xC6, 0xE4, 0xB1, 0x5D, 0x4B, 0x1C, 0x97, 0x9E, 0x66, 0x1E, 0x3D, 0xDC, 0xB4, 0xB9 };
|
||||||
|
|
||||||
|
/* Interface IDeckLinkInputCallback_v11_5_1 - Frame arrival callback. */
|
||||||
|
|
||||||
|
class BMD_PUBLIC IDeckLinkInputCallback_v11_5_1 : public IUnknown
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual HRESULT VideoInputFormatChanged (/* in */ BMDVideoInputFormatChangedEvents notificationEvents, /* in */ IDeckLinkDisplayMode* newDisplayMode, /* in */ BMDDetectedVideoInputFormatFlags detectedSignalFlags) = 0;
|
||||||
|
virtual HRESULT VideoInputFrameArrived (/* in */ IDeckLinkVideoInputFrame_v14_2_1* videoFrame, /* in */ IDeckLinkAudioInputPacket* audioPacket) = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual ~IDeckLinkInputCallback_v11_5_1 () {} // call Release method to drop reference count
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Interface IDeckLinkInput_v11_5_1 - Created by QueryInterface from IDeckLink. */
|
||||||
|
|
||||||
|
class BMD_PUBLIC IDeckLinkInput_v11_5_1 : public IUnknown
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual HRESULT DoesSupportVideoMode (/* in */ BMDVideoConnection connection /* If a value of bmdVideoConnectionUnspecified is specified, the caller does not care about the connection */, /* in */ BMDDisplayMode requestedMode, /* in */ BMDPixelFormat requestedPixelFormat, /* in */ BMDVideoInputConversionMode conversionMode, /* in */ BMDSupportedVideoModeFlags flags, /* out */ BMDDisplayMode* actualMode, /* out */ bool* supported) = 0;
|
||||||
|
virtual HRESULT GetDisplayMode (/* in */ BMDDisplayMode displayMode, /* out */ IDeckLinkDisplayMode** resultDisplayMode) = 0;
|
||||||
|
virtual HRESULT GetDisplayModeIterator (/* out */ IDeckLinkDisplayModeIterator** iterator) = 0;
|
||||||
|
virtual HRESULT SetScreenPreviewCallback (/* in */ IDeckLinkScreenPreviewCallback_v14_2_1* previewCallback) = 0;
|
||||||
|
|
||||||
|
/* Video Input */
|
||||||
|
|
||||||
|
virtual HRESULT EnableVideoInput (/* in */ BMDDisplayMode displayMode, /* in */ BMDPixelFormat pixelFormat, /* in */ BMDVideoInputFlags flags) = 0;
|
||||||
|
virtual HRESULT DisableVideoInput (void) = 0;
|
||||||
|
virtual HRESULT GetAvailableVideoFrameCount (/* out */ uint32_t* availableFrameCount) = 0;
|
||||||
|
virtual HRESULT SetVideoInputFrameMemoryAllocator (/* in */ IDeckLinkMemoryAllocator_v14_2_1* theAllocator) = 0;
|
||||||
|
|
||||||
|
/* Audio Input */
|
||||||
|
|
||||||
|
virtual HRESULT EnableAudioInput (/* in */ BMDAudioSampleRate sampleRate, /* in */ BMDAudioSampleType sampleType, /* in */ uint32_t channelCount) = 0;
|
||||||
|
virtual HRESULT DisableAudioInput (void) = 0;
|
||||||
|
virtual HRESULT GetAvailableAudioSampleFrameCount (/* out */ uint32_t* availableSampleFrameCount) = 0;
|
||||||
|
|
||||||
|
/* Input Control */
|
||||||
|
|
||||||
|
virtual HRESULT StartStreams (void) = 0;
|
||||||
|
virtual HRESULT StopStreams (void) = 0;
|
||||||
|
virtual HRESULT PauseStreams (void) = 0;
|
||||||
|
virtual HRESULT FlushStreams (void) = 0;
|
||||||
|
virtual HRESULT SetCallback (/* in */ IDeckLinkInputCallback_v11_5_1* theCallback) = 0;
|
||||||
|
|
||||||
|
/* Hardware Timing */
|
||||||
|
|
||||||
|
virtual HRESULT GetHardwareReferenceClock (/* in */ BMDTimeScale desiredTimeScale, /* out */ BMDTimeValue* hardwareTime, /* out */ BMDTimeValue* timeInFrame, /* out */ BMDTimeValue* ticksPerFrame) = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual ~IDeckLinkInput_v11_5_1 () {} // call Release method to drop reference count
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /* defined(BMD_DECKLINKAPIVIDEOINPUT_v11_5_1_H) */
|
||||||
118
services/capture/sdk/DeckLinkAPIVideoInput_v14_2_1.h
Normal file
118
services/capture/sdk/DeckLinkAPIVideoInput_v14_2_1.h
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
/* -LICENSE-START-
|
||||||
|
** Copyright (c) 2022 Blackmagic Design
|
||||||
|
**
|
||||||
|
** Permission is hereby granted, free of charge, to any person or organization
|
||||||
|
** obtaining a copy of the software and accompanying documentation (the
|
||||||
|
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||||
|
** and transmit the Software, and to prepare derivative works of the Software,
|
||||||
|
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||||
|
** accordance with:
|
||||||
|
**
|
||||||
|
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||||
|
** Agreement for the Software Development Kit (“EULA”) available at
|
||||||
|
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||||
|
**
|
||||||
|
** (2) if the Software is obtained from any third party, such licensing terms
|
||||||
|
** as notified by that third party,
|
||||||
|
**
|
||||||
|
** and all subject to the following:
|
||||||
|
**
|
||||||
|
** (3) the copyright notices in the Software and this entire statement,
|
||||||
|
** including the above license grant, this restriction and the following
|
||||||
|
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||||
|
** part, and all derivative works of the Software, unless such copies or
|
||||||
|
** derivative works are solely in the form of machine-executable object code
|
||||||
|
** generated by a source language processor.
|
||||||
|
**
|
||||||
|
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||||
|
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||||
|
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||||
|
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
** DEALINGS IN THE SOFTWARE.
|
||||||
|
**
|
||||||
|
** A copy of the Software is available free of charge at
|
||||||
|
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||||
|
**
|
||||||
|
** -LICENSE-END-
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef BMD_DECKLINKAPIVIDEOINPUT_v14_2_1_H
|
||||||
|
#define BMD_DECKLINKAPIVIDEOINPUT_v14_2_1_H
|
||||||
|
|
||||||
|
#include "DeckLinkAPI.h"
|
||||||
|
#include "DeckLinkAPIMemoryAllocator_v14_2_1.h"
|
||||||
|
#include "DeckLinkAPIVideoFrame_v14_2_1.h"
|
||||||
|
#include "DeckLinkAPIScreenPreviewCallback_v14_2_1.h"
|
||||||
|
|
||||||
|
// Type Declarations
|
||||||
|
|
||||||
|
BMD_CONST REFIID IID_IDeckLinkVideoInputFrame_v14_2_1 = /* 05CFE374-537C-4094-9A57-680525118F44 */ { 0x05, 0xCF, 0xE3, 0x74, 0x53, 0x7C, 0x40, 0x94, 0x9A, 0x57, 0x68, 0x05, 0x25, 0x11, 0x8F, 0x44 };
|
||||||
|
BMD_CONST REFIID IID_IDeckLinkInputCallback_v14_2_1 = /* C6FCE4C9-C4E4-4047-82FB-5D238232A902 */ { 0xC6, 0xFC, 0xE4, 0xC9, 0xC4, 0xE4, 0x40, 0x47, 0x82, 0xFB, 0x5D, 0x23, 0x82, 0x32, 0xA9, 0x02 };
|
||||||
|
BMD_CONST REFIID IID_IDeckLinkInput_v14_2_1 = /* C21CDB6E-F414-46E4-A636-80A566E0ED37 */ { 0xC2, 0x1C, 0xDB, 0x6E, 0xF4, 0x14, 0x46, 0xE4, 0xA6, 0x36, 0x80, 0xA5, 0x66, 0xE0, 0xED, 0x37 };
|
||||||
|
|
||||||
|
/* Interface IDeckLinkVideoInputFrame - Provided by the IDeckLinkVideoInput frame arrival callback. */
|
||||||
|
|
||||||
|
class BMD_PUBLIC IDeckLinkVideoInputFrame_v14_2_1 : public IDeckLinkVideoFrame_v14_2_1
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual HRESULT GetStreamTime (/* out */ BMDTimeValue* frameTime, /* out */ BMDTimeValue* frameDuration, /* in */ BMDTimeScale timeScale) = 0;
|
||||||
|
virtual HRESULT GetHardwareReferenceTimestamp (/* in */ BMDTimeScale timeScale, /* out */ BMDTimeValue* frameTime, /* out */ BMDTimeValue* frameDuration) = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual ~IDeckLinkVideoInputFrame_v14_2_1 () {} // call Release method to drop reference count
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Interface IDeckLinkInputCallback_v14_2_1 - Frame arrival callback. */
|
||||||
|
|
||||||
|
class BMD_PUBLIC IDeckLinkInputCallback_v14_2_1 : public IUnknown
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual HRESULT VideoInputFormatChanged (/* in */ BMDVideoInputFormatChangedEvents notificationEvents, /* in */ IDeckLinkDisplayMode* newDisplayMode, /* in */ BMDDetectedVideoInputFormatFlags detectedSignalFlags) = 0;
|
||||||
|
virtual HRESULT VideoInputFrameArrived (/* in */ IDeckLinkVideoInputFrame_v14_2_1* videoFrame, /* in */ IDeckLinkAudioInputPacket* audioPacket) = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual ~IDeckLinkInputCallback_v14_2_1 () {} // call Release method to drop reference count
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Interface IDeckLinkInput - Created by QueryInterface from IDeckLink. */
|
||||||
|
|
||||||
|
class BMD_PUBLIC IDeckLinkInput_v14_2_1 : public IUnknown
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual HRESULT DoesSupportVideoMode (/* in */ BMDVideoConnection connection /* If a value of bmdVideoConnectionUnspecified is specified, the caller does not care about the connection */, /* in */ BMDDisplayMode requestedMode, /* in */ BMDPixelFormat requestedPixelFormat, /* in */ BMDVideoInputConversionMode conversionMode, /* in */ BMDSupportedVideoModeFlags flags, /* out */ BMDDisplayMode* actualMode, /* out */ bool* supported) = 0;
|
||||||
|
virtual HRESULT GetDisplayMode (/* in */ BMDDisplayMode displayMode, /* out */ IDeckLinkDisplayMode** resultDisplayMode) = 0;
|
||||||
|
virtual HRESULT GetDisplayModeIterator (/* out */ IDeckLinkDisplayModeIterator** iterator) = 0;
|
||||||
|
virtual HRESULT SetScreenPreviewCallback (/* in */ IDeckLinkScreenPreviewCallback_v14_2_1* previewCallback) = 0;
|
||||||
|
|
||||||
|
/* Video Input */
|
||||||
|
|
||||||
|
virtual HRESULT EnableVideoInput (/* in */ BMDDisplayMode displayMode, /* in */ BMDPixelFormat pixelFormat, /* in */ BMDVideoInputFlags flags) = 0;
|
||||||
|
virtual HRESULT DisableVideoInput (void) = 0;
|
||||||
|
virtual HRESULT GetAvailableVideoFrameCount (/* out */ uint32_t* availableFrameCount) = 0;
|
||||||
|
virtual HRESULT SetVideoInputFrameMemoryAllocator (/* in */ IDeckLinkMemoryAllocator_v14_2_1* theAllocator) = 0;
|
||||||
|
|
||||||
|
/* Audio Input */
|
||||||
|
|
||||||
|
virtual HRESULT EnableAudioInput (/* in */ BMDAudioSampleRate sampleRate, /* in */ BMDAudioSampleType sampleType, /* in */ uint32_t channelCount) = 0;
|
||||||
|
virtual HRESULT DisableAudioInput (void) = 0;
|
||||||
|
virtual HRESULT GetAvailableAudioSampleFrameCount (/* out */ uint32_t* availableSampleFrameCount) = 0;
|
||||||
|
|
||||||
|
/* Input Control */
|
||||||
|
|
||||||
|
virtual HRESULT StartStreams (void) = 0;
|
||||||
|
virtual HRESULT StopStreams (void) = 0;
|
||||||
|
virtual HRESULT PauseStreams (void) = 0;
|
||||||
|
virtual HRESULT FlushStreams (void) = 0;
|
||||||
|
virtual HRESULT SetCallback (/* in */ IDeckLinkInputCallback_v14_2_1* theCallback) = 0;
|
||||||
|
|
||||||
|
/* Hardware Timing */
|
||||||
|
|
||||||
|
virtual HRESULT GetHardwareReferenceClock (/* in */ BMDTimeScale desiredTimeScale, /* out */ BMDTimeValue* hardwareTime, /* out */ BMDTimeValue* timeInFrame, /* out */ BMDTimeValue* ticksPerFrame) = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual ~IDeckLinkInput_v14_2_1 () {} // call Release method to drop reference count
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /* defined(BMD_DECKLINKAPIVIDEOINPUT_v14_2_1_H) */
|
||||||
86
services/capture/sdk/DeckLinkAPIVideoInput_v15_3_1.h
Normal file
86
services/capture/sdk/DeckLinkAPIVideoInput_v15_3_1.h
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
/* -LICENSE-START-
|
||||||
|
** Copyright (c) 2025 Blackmagic Design
|
||||||
|
**
|
||||||
|
** Permission is hereby granted, free of charge, to any person or organization
|
||||||
|
** obtaining a copy of the software and accompanying documentation (the
|
||||||
|
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||||
|
** and transmit the Software, and to prepare derivative works of the Software,
|
||||||
|
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||||
|
** accordance with:
|
||||||
|
**
|
||||||
|
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||||
|
** Agreement for the Software Development Kit (“EULA”) available at
|
||||||
|
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||||
|
**
|
||||||
|
** (2) if the Software is obtained from any third party, such licensing terms
|
||||||
|
** as notified by that third party,
|
||||||
|
**
|
||||||
|
** and all subject to the following:
|
||||||
|
**
|
||||||
|
** (3) the copyright notices in the Software and this entire statement,
|
||||||
|
** including the above license grant, this restriction and the following
|
||||||
|
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||||
|
** part, and all derivative works of the Software, unless such copies or
|
||||||
|
** derivative works are solely in the form of machine-executable object code
|
||||||
|
** generated by a source language processor.
|
||||||
|
**
|
||||||
|
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||||
|
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||||
|
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||||
|
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
** DEALINGS IN THE SOFTWARE.
|
||||||
|
**
|
||||||
|
** A copy of the Software is available free of charge at
|
||||||
|
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||||
|
**
|
||||||
|
** -LICENSE-END-
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "DeckLinkAPI_v15_3_1.h"
|
||||||
|
|
||||||
|
// Type Declarations
|
||||||
|
|
||||||
|
BMD_CONST REFIID IID_IDeckLinkInput_v15_3_1 = /* 4095DB82-E294-4B8C-AAA8-3B9E80C49336 */ { 0x40,0x95,0xDB,0x82,0xE2,0x94,0x4B,0x8C,0xAA,0xA8,0x3B,0x9E,0x80,0xC4,0x93,0x36 };
|
||||||
|
|
||||||
|
/* Interface IDeckLinkInput_v15_3_1 - Created by QueryInterface from IDeckLink. */
|
||||||
|
|
||||||
|
class BMD_PUBLIC IDeckLinkInput_v15_3_1 : public IUnknown
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual HRESULT DoesSupportVideoMode (/* in */ BMDVideoConnection connection /* If a value of bmdVideoConnectionUnspecified is specified, the caller does not care about the connection */, /* in */ BMDDisplayMode requestedMode, /* in */ BMDPixelFormat requestedPixelFormat, /* in */ BMDVideoInputConversionMode conversionMode, /* in */ BMDSupportedVideoModeFlags flags, /* out */ BMDDisplayMode* actualMode, /* out */ bool* supported) = 0;
|
||||||
|
virtual HRESULT GetDisplayMode (/* in */ BMDDisplayMode displayMode, /* out */ IDeckLinkDisplayMode** resultDisplayMode) = 0;
|
||||||
|
virtual HRESULT GetDisplayModeIterator (/* out */ IDeckLinkDisplayModeIterator** iterator) = 0;
|
||||||
|
virtual HRESULT SetScreenPreviewCallback (/* in */ IDeckLinkScreenPreviewCallback* previewCallback) = 0;
|
||||||
|
|
||||||
|
/* Video Input */
|
||||||
|
|
||||||
|
virtual HRESULT EnableVideoInput (/* in */ BMDDisplayMode displayMode, /* in */ BMDPixelFormat pixelFormat, /* in */ BMDVideoInputFlags flags) = 0;
|
||||||
|
virtual HRESULT EnableVideoInputWithAllocatorProvider (/* in */ BMDDisplayMode displayMode, /* in */ BMDPixelFormat pixelFormat, /* in */ BMDVideoInputFlags flags, /* in */ IDeckLinkVideoBufferAllocatorProvider_v15_3_1* allocatorProvider) = 0;
|
||||||
|
virtual HRESULT DisableVideoInput (void) = 0;
|
||||||
|
virtual HRESULT GetAvailableVideoFrameCount (/* out */ uint32_t* availableFrameCount) = 0;
|
||||||
|
|
||||||
|
/* Audio Input */
|
||||||
|
|
||||||
|
virtual HRESULT EnableAudioInput (/* in */ BMDAudioSampleRate sampleRate, /* in */ BMDAudioSampleType sampleType, /* in */ uint32_t channelCount) = 0;
|
||||||
|
virtual HRESULT DisableAudioInput (void) = 0;
|
||||||
|
virtual HRESULT GetAvailableAudioSampleFrameCount (/* out */ uint32_t* availableSampleFrameCount) = 0;
|
||||||
|
|
||||||
|
/* Input Control */
|
||||||
|
|
||||||
|
virtual HRESULT StartStreams (void) = 0;
|
||||||
|
virtual HRESULT StopStreams (void) = 0;
|
||||||
|
virtual HRESULT PauseStreams (void) = 0;
|
||||||
|
virtual HRESULT FlushStreams (void) = 0;
|
||||||
|
virtual HRESULT SetCallback (/* in */ IDeckLinkInputCallback* theCallback) = 0;
|
||||||
|
|
||||||
|
/* Hardware Timing */
|
||||||
|
|
||||||
|
virtual HRESULT GetHardwareReferenceClock (/* in */ BMDTimeScale desiredTimeScale, /* out */ BMDTimeValue* hardwareTime, /* out */ BMDTimeValue* timeInFrame, /* out */ BMDTimeValue* ticksPerFrame) = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual ~IDeckLinkInput_v15_3_1 () {} // call Release method to drop reference count
|
||||||
|
};
|
||||||
109
services/capture/sdk/DeckLinkAPIVideoOutput_v10_11.h
Normal file
109
services/capture/sdk/DeckLinkAPIVideoOutput_v10_11.h
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
/* -LICENSE-START-
|
||||||
|
** Copyright (c) 2017 Blackmagic Design
|
||||||
|
**
|
||||||
|
** Permission is hereby granted, free of charge, to any person or organization
|
||||||
|
** obtaining a copy of the software and accompanying documentation (the
|
||||||
|
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||||
|
** and transmit the Software, and to prepare derivative works of the Software,
|
||||||
|
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||||
|
** accordance with:
|
||||||
|
**
|
||||||
|
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||||
|
** Agreement for the Software Development Kit ("EULA") available at
|
||||||
|
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||||
|
**
|
||||||
|
** (2) if the Software is obtained from any third party, such licensing terms
|
||||||
|
** as notified by that third party,
|
||||||
|
**
|
||||||
|
** and all subject to the following:
|
||||||
|
**
|
||||||
|
** (3) the copyright notices in the Software and this entire statement,
|
||||||
|
** including the above license grant, this restriction and the following
|
||||||
|
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||||
|
** part, and all derivative works of the Software, unless such copies or
|
||||||
|
** derivative works are solely in the form of machine-executable object code
|
||||||
|
** generated by a source language processor.
|
||||||
|
**
|
||||||
|
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||||
|
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||||
|
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||||
|
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
** DEALINGS IN THE SOFTWARE.
|
||||||
|
**
|
||||||
|
** A copy of the Software is available free of charge at
|
||||||
|
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||||
|
**
|
||||||
|
** -LICENSE-END-
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef BMD_DECKLINKAPIVIDEOOUTPUT_v10_11_H
|
||||||
|
#define BMD_DECKLINKAPIVIDEOOUTPUT_v10_11_H
|
||||||
|
|
||||||
|
#include "DeckLinkAPI.h"
|
||||||
|
#include "DeckLinkAPI_v10_11.h"
|
||||||
|
#include "DeckLinkAPIVideoInput_v14_2_1.h"
|
||||||
|
#include "DeckLinkAPIVideoOutput_v14_2_1.h"
|
||||||
|
|
||||||
|
// Type Declarations
|
||||||
|
BMD_CONST REFIID IID_IDeckLinkOutput_v10_11 = /* CC5C8A6E-3F2F-4B3A-87EA-FD78AF300564 */ {0xCC,0x5C,0x8A,0x6E,0x3F,0x2F,0x4B,0x3A,0x87,0xEA,0xFD,0x78,0xAF,0x30,0x05,0x64};
|
||||||
|
|
||||||
|
/* Interface IDeckLinkOutput_v10_11 - DeckLink output interface. */
|
||||||
|
|
||||||
|
class IDeckLinkOutput_v10_11 : public IUnknown
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual HRESULT DoesSupportVideoMode (/* in */ BMDDisplayMode displayMode, /* in */ BMDPixelFormat pixelFormat, /* in */ BMDVideoOutputFlags flags, /* out */ BMDDisplayModeSupport_v10_11 *result, /* out */ IDeckLinkDisplayMode **resultDisplayMode) = 0;
|
||||||
|
virtual HRESULT GetDisplayModeIterator (/* out */ IDeckLinkDisplayModeIterator **iterator) = 0;
|
||||||
|
|
||||||
|
virtual HRESULT SetScreenPreviewCallback (/* in */ IDeckLinkScreenPreviewCallback_v14_2_1 *previewCallback) = 0;
|
||||||
|
|
||||||
|
/* Video Output */
|
||||||
|
|
||||||
|
virtual HRESULT EnableVideoOutput (/* in */ BMDDisplayMode displayMode, /* in */ BMDVideoOutputFlags flags) = 0;
|
||||||
|
virtual HRESULT DisableVideoOutput (void) = 0;
|
||||||
|
|
||||||
|
virtual HRESULT SetVideoOutputFrameMemoryAllocator (/* in */ IDeckLinkMemoryAllocator_v14_2_1 *theAllocator) = 0;
|
||||||
|
virtual HRESULT CreateVideoFrame (/* in */ int32_t width, /* in */ int32_t height, /* in */ int32_t rowBytes, /* in */ BMDPixelFormat pixelFormat, /* in */ BMDFrameFlags flags, /* out */ IDeckLinkMutableVideoFrame_v14_2_1 **outFrame) = 0;
|
||||||
|
virtual HRESULT CreateAncillaryData (/* in */ BMDPixelFormat pixelFormat, /* out */ IDeckLinkVideoFrameAncillary **outBuffer) = 0;
|
||||||
|
|
||||||
|
virtual HRESULT DisplayVideoFrameSync (/* in */ IDeckLinkVideoFrame_v14_2_1 *theFrame) = 0;
|
||||||
|
virtual HRESULT ScheduleVideoFrame (/* in */ IDeckLinkVideoFrame_v14_2_1 *theFrame, /* in */ BMDTimeValue displayTime, /* in */ BMDTimeValue displayDuration, /* in */ BMDTimeScale timeScale) = 0;
|
||||||
|
virtual HRESULT SetScheduledFrameCompletionCallback (/* in */ IDeckLinkVideoOutputCallback_v14_2_1 *theCallback) = 0;
|
||||||
|
virtual HRESULT GetBufferedVideoFrameCount (/* out */ uint32_t *bufferedFrameCount) = 0;
|
||||||
|
|
||||||
|
/* Audio Output */
|
||||||
|
|
||||||
|
virtual HRESULT EnableAudioOutput (/* in */ BMDAudioSampleRate sampleRate, /* in */ BMDAudioSampleType sampleType, /* in */ uint32_t channelCount, /* in */ BMDAudioOutputStreamType streamType) = 0;
|
||||||
|
virtual HRESULT DisableAudioOutput (void) = 0;
|
||||||
|
|
||||||
|
virtual HRESULT WriteAudioSamplesSync (/* in */ void *buffer, /* in */ uint32_t sampleFrameCount, /* out */ uint32_t *sampleFramesWritten) = 0;
|
||||||
|
|
||||||
|
virtual HRESULT BeginAudioPreroll (void) = 0;
|
||||||
|
virtual HRESULT EndAudioPreroll (void) = 0;
|
||||||
|
virtual HRESULT ScheduleAudioSamples (/* in */ void *buffer, /* in */ uint32_t sampleFrameCount, /* in */ BMDTimeValue streamTime, /* in */ BMDTimeScale timeScale, /* out */ uint32_t *sampleFramesWritten) = 0;
|
||||||
|
|
||||||
|
virtual HRESULT GetBufferedAudioSampleFrameCount (/* out */ uint32_t *bufferedSampleFrameCount) = 0;
|
||||||
|
virtual HRESULT FlushBufferedAudioSamples (void) = 0;
|
||||||
|
|
||||||
|
virtual HRESULT SetAudioCallback (/* in */ IDeckLinkAudioOutputCallback *theCallback) = 0;
|
||||||
|
|
||||||
|
/* Output Control */
|
||||||
|
|
||||||
|
virtual HRESULT StartScheduledPlayback (/* in */ BMDTimeValue playbackStartTime, /* in */ BMDTimeScale timeScale, /* in */ double playbackSpeed) = 0;
|
||||||
|
virtual HRESULT StopScheduledPlayback (/* in */ BMDTimeValue stopPlaybackAtTime, /* out */ BMDTimeValue *actualStopTime, /* in */ BMDTimeScale timeScale) = 0;
|
||||||
|
virtual HRESULT IsScheduledPlaybackRunning (/* out */ bool *active) = 0;
|
||||||
|
virtual HRESULT GetScheduledStreamTime (/* in */ BMDTimeScale desiredTimeScale, /* out */ BMDTimeValue *streamTime, /* out */ double *playbackSpeed) = 0;
|
||||||
|
virtual HRESULT GetReferenceStatus (/* out */ BMDReferenceStatus *referenceStatus) = 0;
|
||||||
|
|
||||||
|
/* Hardware Timing */
|
||||||
|
|
||||||
|
virtual HRESULT GetHardwareReferenceClock (/* in */ BMDTimeScale desiredTimeScale, /* out */ BMDTimeValue *hardwareTime, /* out */ BMDTimeValue *timeInFrame, /* out */ BMDTimeValue *ticksPerFrame) = 0;
|
||||||
|
virtual HRESULT GetFrameCompletionReferenceTimestamp (/* in */ IDeckLinkVideoFrame_v14_2_1 *theFrame, /* in */ BMDTimeScale desiredTimeScale, /* out */ BMDTimeValue *frameCompletionTimestamp) = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual ~IDeckLinkOutput_v10_11 () {} // call Release method to drop reference count
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /* defined(BMD_DECKLINKAPIVIDEOOUTPUT_v10_11_H) */
|
||||||
101
services/capture/sdk/DeckLinkAPIVideoOutput_v11_4.h
Normal file
101
services/capture/sdk/DeckLinkAPIVideoOutput_v11_4.h
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
/* -LICENSE-START-
|
||||||
|
** Copyright (c) 2019 Blackmagic Design
|
||||||
|
**
|
||||||
|
** Permission is hereby granted, free of charge, to any person or organization
|
||||||
|
** obtaining a copy of the software and accompanying documentation (the
|
||||||
|
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||||
|
** and transmit the Software, and to prepare derivative works of the Software,
|
||||||
|
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||||
|
** accordance with:
|
||||||
|
**
|
||||||
|
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||||
|
** Agreement for the Software Development Kit ("EULA") available at
|
||||||
|
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||||
|
**
|
||||||
|
** (2) if the Software is obtained from any third party, such licensing terms
|
||||||
|
** as notified by that third party,
|
||||||
|
**
|
||||||
|
** and all subject to the following:
|
||||||
|
**
|
||||||
|
** (3) the copyright notices in the Software and this entire statement,
|
||||||
|
** including the above license grant, this restriction and the following
|
||||||
|
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||||
|
** part, and all derivative works of the Software, unless such copies or
|
||||||
|
** derivative works are solely in the form of machine-executable object code
|
||||||
|
** generated by a source language processor.
|
||||||
|
**
|
||||||
|
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||||
|
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||||
|
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||||
|
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
** DEALINGS IN THE SOFTWARE.
|
||||||
|
**
|
||||||
|
** A copy of the Software is available free of charge at
|
||||||
|
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||||
|
**
|
||||||
|
** -LICENSE-END-
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef BMD_DECKLINKAPIVIDEOOUTPUT_v11_4_H
|
||||||
|
#define BMD_DECKLINKAPIVIDEOOUTPUT_v11_4_H
|
||||||
|
|
||||||
|
#include "DeckLinkAPI.h"
|
||||||
|
#include "DeckLinkAPIVideoOutput_v14_2_1.h"
|
||||||
|
|
||||||
|
// Type Declarations
|
||||||
|
BMD_CONST REFIID IID_IDeckLinkOutput_v11_4 = /* 065A0F6C-C508-4D0D-B919-F5EB0EBFC96B */ { 0x06,0x5A,0x0F,0x6C,0xC5,0x08,0x4D,0x0D,0xB9,0x19,0xF5,0xEB,0x0E,0xBF,0xC9,0x6B };
|
||||||
|
|
||||||
|
/* Interface IDeckLinkOutput_v11_4 - Created by QueryInterface from IDeckLink. */
|
||||||
|
|
||||||
|
class BMD_PUBLIC IDeckLinkOutput_v11_4 : public IUnknown
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual HRESULT DoesSupportVideoMode (/* in */ BMDVideoConnection connection /* If a value of 0 is specified, the caller does not care about the connection */, /* in */ BMDDisplayMode requestedMode, /* in */ BMDPixelFormat requestedPixelFormat, /* in */ BMDSupportedVideoModeFlags flags, /* out */ BMDDisplayMode* actualMode, /* out */ bool* supported) = 0;
|
||||||
|
virtual HRESULT GetDisplayMode (/* in */ BMDDisplayMode displayMode, /* out */ IDeckLinkDisplayMode** resultDisplayMode) = 0;
|
||||||
|
virtual HRESULT GetDisplayModeIterator (/* out */ IDeckLinkDisplayModeIterator** iterator) = 0;
|
||||||
|
virtual HRESULT SetScreenPreviewCallback (/* in */ IDeckLinkScreenPreviewCallback_v14_2_1* previewCallback) = 0;
|
||||||
|
|
||||||
|
/* Video Output */
|
||||||
|
|
||||||
|
virtual HRESULT EnableVideoOutput (/* in */ BMDDisplayMode displayMode, /* in */ BMDVideoOutputFlags flags) = 0;
|
||||||
|
virtual HRESULT DisableVideoOutput (void) = 0;
|
||||||
|
virtual HRESULT SetVideoOutputFrameMemoryAllocator (/* in */ IDeckLinkMemoryAllocator_v14_2_1* theAllocator) = 0;
|
||||||
|
virtual HRESULT CreateVideoFrame (/* in */ int32_t width, /* in */ int32_t height, /* in */ int32_t rowBytes, /* in */ BMDPixelFormat pixelFormat, /* in */ BMDFrameFlags flags, /* out */ IDeckLinkMutableVideoFrame_v14_2_1** outFrame) = 0;
|
||||||
|
virtual HRESULT CreateAncillaryData (/* in */ BMDPixelFormat pixelFormat, /* out */ IDeckLinkVideoFrameAncillary** outBuffer) = 0; // Use of IDeckLinkVideoFrameAncillaryPackets is preferred
|
||||||
|
virtual HRESULT DisplayVideoFrameSync (/* in */ IDeckLinkVideoFrame_v14_2_1* theFrame) = 0;
|
||||||
|
virtual HRESULT ScheduleVideoFrame (/* in */ IDeckLinkVideoFrame_v14_2_1* theFrame, /* in */ BMDTimeValue displayTime, /* in */ BMDTimeValue displayDuration, /* in */ BMDTimeScale timeScale) = 0;
|
||||||
|
virtual HRESULT SetScheduledFrameCompletionCallback (/* in */ IDeckLinkVideoOutputCallback_v14_2_1* theCallback) = 0;
|
||||||
|
virtual HRESULT GetBufferedVideoFrameCount (/* out */ uint32_t* bufferedFrameCount) = 0;
|
||||||
|
|
||||||
|
/* Audio Output */
|
||||||
|
|
||||||
|
virtual HRESULT EnableAudioOutput (/* in */ BMDAudioSampleRate sampleRate, /* in */ BMDAudioSampleType sampleType, /* in */ uint32_t channelCount, /* in */ BMDAudioOutputStreamType streamType) = 0;
|
||||||
|
virtual HRESULT DisableAudioOutput (void) = 0;
|
||||||
|
virtual HRESULT WriteAudioSamplesSync (/* in */ void* buffer, /* in */ uint32_t sampleFrameCount, /* out */ uint32_t* sampleFramesWritten) = 0;
|
||||||
|
virtual HRESULT BeginAudioPreroll (void) = 0;
|
||||||
|
virtual HRESULT EndAudioPreroll (void) = 0;
|
||||||
|
virtual HRESULT ScheduleAudioSamples (/* in */ void* buffer, /* in */ uint32_t sampleFrameCount, /* in */ BMDTimeValue streamTime, /* in */ BMDTimeScale timeScale, /* out */ uint32_t* sampleFramesWritten) = 0;
|
||||||
|
virtual HRESULT GetBufferedAudioSampleFrameCount (/* out */ uint32_t* bufferedSampleFrameCount) = 0;
|
||||||
|
virtual HRESULT FlushBufferedAudioSamples (void) = 0;
|
||||||
|
virtual HRESULT SetAudioCallback (/* in */ IDeckLinkAudioOutputCallback* theCallback) = 0;
|
||||||
|
|
||||||
|
/* Output Control */
|
||||||
|
|
||||||
|
virtual HRESULT StartScheduledPlayback (/* in */ BMDTimeValue playbackStartTime, /* in */ BMDTimeScale timeScale, /* in */ double playbackSpeed) = 0;
|
||||||
|
virtual HRESULT StopScheduledPlayback (/* in */ BMDTimeValue stopPlaybackAtTime, /* out */ BMDTimeValue* actualStopTime, /* in */ BMDTimeScale timeScale) = 0;
|
||||||
|
virtual HRESULT IsScheduledPlaybackRunning (/* out */ bool* active) = 0;
|
||||||
|
virtual HRESULT GetScheduledStreamTime (/* in */ BMDTimeScale desiredTimeScale, /* out */ BMDTimeValue* streamTime, /* out */ double* playbackSpeed) = 0;
|
||||||
|
virtual HRESULT GetReferenceStatus (/* out */ BMDReferenceStatus* referenceStatus) = 0;
|
||||||
|
|
||||||
|
/* Hardware Timing */
|
||||||
|
|
||||||
|
virtual HRESULT GetHardwareReferenceClock (/* in */ BMDTimeScale desiredTimeScale, /* out */ BMDTimeValue* hardwareTime, /* out */ BMDTimeValue* timeInFrame, /* out */ BMDTimeValue* ticksPerFrame) = 0;
|
||||||
|
virtual HRESULT GetFrameCompletionReferenceTimestamp (/* in */ IDeckLinkVideoFrame_v14_2_1* theFrame, /* in */ BMDTimeScale desiredTimeScale, /* out */ BMDTimeValue* frameCompletionTimestamp) = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual ~IDeckLinkOutput_v11_4 () {} // call Release method to drop reference count
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /* defined(BMD_DECKLINKAPIVIDEOOUTPUT_v11_4_H) */
|
||||||
133
services/capture/sdk/DeckLinkAPIVideoOutput_v14_2_1.h
Normal file
133
services/capture/sdk/DeckLinkAPIVideoOutput_v14_2_1.h
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
/* -LICENSE-START-
|
||||||
|
** Copyright (c) 2022 Blackmagic Design
|
||||||
|
**
|
||||||
|
** Permission is hereby granted, free of charge, to any person or organization
|
||||||
|
** obtaining a copy of the software and accompanying documentation (the
|
||||||
|
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||||
|
** and transmit the Software, and to prepare derivative works of the Software,
|
||||||
|
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||||
|
** accordance with:
|
||||||
|
**
|
||||||
|
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||||
|
** Agreement for the Software Development Kit (“EULA”) available at
|
||||||
|
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||||
|
**
|
||||||
|
** (2) if the Software is obtained from any third party, such licensing terms
|
||||||
|
** as notified by that third party,
|
||||||
|
**
|
||||||
|
** and all subject to the following:
|
||||||
|
**
|
||||||
|
** (3) the copyright notices in the Software and this entire statement,
|
||||||
|
** including the above license grant, this restriction and the following
|
||||||
|
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||||
|
** part, and all derivative works of the Software, unless such copies or
|
||||||
|
** derivative works are solely in the form of machine-executable object code
|
||||||
|
** generated by a source language processor.
|
||||||
|
**
|
||||||
|
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||||
|
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||||
|
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||||
|
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
** DEALINGS IN THE SOFTWARE.
|
||||||
|
**
|
||||||
|
** A copy of the Software is available free of charge at
|
||||||
|
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||||
|
**
|
||||||
|
** -LICENSE-END-
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef BMD_DECKLINKAPIVIDEOOUTPUT_v14_2_1_H
|
||||||
|
#define BMD_DECKLINKAPIVIDEOOUTPUT_v14_2_1_H
|
||||||
|
|
||||||
|
#include "DeckLinkAPI.h"
|
||||||
|
#include "DeckLinkAPIMemoryAllocator_v14_2_1.h"
|
||||||
|
#include "DeckLinkAPIVideoFrame_v14_2_1.h"
|
||||||
|
#include "DeckLinkAPIScreenPreviewCallback_v14_2_1.h"
|
||||||
|
|
||||||
|
// Type Declarations
|
||||||
|
|
||||||
|
BMD_CONST REFIID IID_IDeckLinkMutableVideoFrame_v14_2_1 = /* 69E2639F-40DA-4E19-B6F2-20ACE815C390 */ { 0x69, 0xE2, 0x63, 0x9F, 0x40, 0xDA, 0x4E, 0x19, 0xB6, 0xF2, 0x20, 0xAC, 0xE8, 0x15, 0xC3, 0x90 };
|
||||||
|
BMD_CONST REFIID IID_IDeckLinkVideoOutputCallback_v14_2_1 = /* 20AA5225-1958-47CB-820B-80A8D521A6EE */ { 0x20, 0xAA, 0x52, 0x25, 0x19, 0x58, 0x47, 0xCB, 0x82, 0x0B, 0x80, 0xA8, 0xD5, 0x21, 0xA6, 0xEE };
|
||||||
|
BMD_CONST REFIID IID_IDeckLinkOutput_v14_2_1 = /* BE2D9020-461E-442F-84B7-E949CB953B9D */ { 0xBE, 0x2D, 0x90, 0x20, 0x46, 0x1E, 0x44, 0x2F, 0x84, 0xB7, 0xE9, 0x49, 0xCB, 0x95, 0x3B, 0x9D };
|
||||||
|
|
||||||
|
/* Interface IDeckLinkMutableVideoFrame - Created by IDeckLinkOutput::CreateVideoFrame. */
|
||||||
|
|
||||||
|
class BMD_PUBLIC IDeckLinkMutableVideoFrame_v14_2_1 : public IDeckLinkVideoFrame_v14_2_1
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual HRESULT SetFlags (/* in */ BMDFrameFlags newFlags) = 0;
|
||||||
|
virtual HRESULT SetTimecode (/* in */ BMDTimecodeFormat format, /* in */ IDeckLinkTimecode* timecode) = 0;
|
||||||
|
virtual HRESULT SetTimecodeFromComponents (/* in */ BMDTimecodeFormat format, /* in */ uint8_t hours, /* in */ uint8_t minutes, /* in */ uint8_t seconds, /* in */ uint8_t frames, /* in */ BMDTimecodeFlags flags) = 0;
|
||||||
|
virtual HRESULT SetAncillaryData (/* in */ IDeckLinkVideoFrameAncillary* ancillary) = 0;
|
||||||
|
virtual HRESULT SetTimecodeUserBits (/* in */ BMDTimecodeFormat format, /* in */ BMDTimecodeUserBits userBits) = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual ~IDeckLinkMutableVideoFrame_v14_2_1 () {} // call Release method to drop reference count
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Interface IDeckLinkVideoOutputCallback - Frame completion callback. */
|
||||||
|
|
||||||
|
class BMD_PUBLIC IDeckLinkVideoOutputCallback_v14_2_1 : public IUnknown
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual HRESULT ScheduledFrameCompleted (/* in */ IDeckLinkVideoFrame_v14_2_1* completedFrame, /* in */ BMDOutputFrameCompletionResult result) = 0;
|
||||||
|
virtual HRESULT ScheduledPlaybackHasStopped (void) = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual ~IDeckLinkVideoOutputCallback_v14_2_1 () {} // call Release method to drop reference count
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Interface IDeckLinkOutput - Created by QueryInterface from IDeckLink. */
|
||||||
|
|
||||||
|
class BMD_PUBLIC IDeckLinkOutput_v14_2_1 : public IUnknown
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual HRESULT DoesSupportVideoMode (/* in */ BMDVideoConnection connection /* If a value of bmdVideoConnectionUnspecified is specified, the caller does not care about the connection */, /* in */ BMDDisplayMode requestedMode, /* in */ BMDPixelFormat requestedPixelFormat, /* in */ BMDVideoOutputConversionMode conversionMode, /* in */ BMDSupportedVideoModeFlags flags, /* out */ BMDDisplayMode* actualMode, /* out */ bool* supported) = 0;
|
||||||
|
virtual HRESULT GetDisplayMode (/* in */ BMDDisplayMode displayMode, /* out */ IDeckLinkDisplayMode** resultDisplayMode) = 0;
|
||||||
|
virtual HRESULT GetDisplayModeIterator (/* out */ IDeckLinkDisplayModeIterator** iterator) = 0;
|
||||||
|
virtual HRESULT SetScreenPreviewCallback (/* in */ IDeckLinkScreenPreviewCallback_v14_2_1* previewCallback) = 0;
|
||||||
|
|
||||||
|
/* Video Output */
|
||||||
|
|
||||||
|
virtual HRESULT EnableVideoOutput (/* in */ BMDDisplayMode displayMode, /* in */ BMDVideoOutputFlags flags) = 0;
|
||||||
|
virtual HRESULT DisableVideoOutput (void) = 0;
|
||||||
|
virtual HRESULT SetVideoOutputFrameMemoryAllocator (/* in */ IDeckLinkMemoryAllocator_v14_2_1* theAllocator) = 0;
|
||||||
|
virtual HRESULT CreateVideoFrame (/* in */ int32_t width, /* in */ int32_t height, /* in */ int32_t rowBytes, /* in */ BMDPixelFormat pixelFormat, /* in */ BMDFrameFlags flags, /* out */ IDeckLinkMutableVideoFrame_v14_2_1** outFrame) = 0;
|
||||||
|
virtual HRESULT CreateAncillaryData (/* in */ BMDPixelFormat pixelFormat, /* out */ IDeckLinkVideoFrameAncillary** outBuffer) = 0; // Use of IDeckLinkVideoFrameAncillaryPackets is preferred
|
||||||
|
virtual HRESULT DisplayVideoFrameSync (/* in */ IDeckLinkVideoFrame_v14_2_1* theFrame) = 0;
|
||||||
|
virtual HRESULT ScheduleVideoFrame (/* in */ IDeckLinkVideoFrame_v14_2_1* theFrame, /* in */ BMDTimeValue displayTime, /* in */ BMDTimeValue displayDuration, /* in */ BMDTimeScale timeScale) = 0;
|
||||||
|
virtual HRESULT SetScheduledFrameCompletionCallback (/* in */ IDeckLinkVideoOutputCallback_v14_2_1* theCallback) = 0;
|
||||||
|
virtual HRESULT GetBufferedVideoFrameCount (/* out */ uint32_t* bufferedFrameCount) = 0;
|
||||||
|
|
||||||
|
/* Audio Output */
|
||||||
|
|
||||||
|
virtual HRESULT EnableAudioOutput (/* in */ BMDAudioSampleRate sampleRate, /* in */ BMDAudioSampleType sampleType, /* in */ uint32_t channelCount, /* in */ BMDAudioOutputStreamType streamType) = 0;
|
||||||
|
virtual HRESULT DisableAudioOutput (void) = 0;
|
||||||
|
virtual HRESULT WriteAudioSamplesSync (/* in */ void* buffer, /* in */ uint32_t sampleFrameCount, /* out */ uint32_t* sampleFramesWritten) = 0;
|
||||||
|
virtual HRESULT BeginAudioPreroll (void) = 0;
|
||||||
|
virtual HRESULT EndAudioPreroll (void) = 0;
|
||||||
|
virtual HRESULT ScheduleAudioSamples (/* in */ void* buffer, /* in */ uint32_t sampleFrameCount, /* in */ BMDTimeValue streamTime, /* in */ BMDTimeScale timeScale, /* out */ uint32_t* sampleFramesWritten) = 0;
|
||||||
|
virtual HRESULT GetBufferedAudioSampleFrameCount (/* out */ uint32_t* bufferedSampleFrameCount) = 0;
|
||||||
|
virtual HRESULT FlushBufferedAudioSamples (void) = 0;
|
||||||
|
virtual HRESULT SetAudioCallback (/* in */ IDeckLinkAudioOutputCallback* theCallback) = 0;
|
||||||
|
|
||||||
|
/* Output Control */
|
||||||
|
|
||||||
|
virtual HRESULT StartScheduledPlayback (/* in */ BMDTimeValue playbackStartTime, /* in */ BMDTimeScale timeScale, /* in */ double playbackSpeed) = 0;
|
||||||
|
virtual HRESULT StopScheduledPlayback (/* in */ BMDTimeValue stopPlaybackAtTime, /* out */ BMDTimeValue* actualStopTime, /* in */ BMDTimeScale timeScale) = 0;
|
||||||
|
virtual HRESULT IsScheduledPlaybackRunning (/* out */ bool* active) = 0;
|
||||||
|
virtual HRESULT GetScheduledStreamTime (/* in */ BMDTimeScale desiredTimeScale, /* out */ BMDTimeValue* streamTime, /* out */ double* playbackSpeed) = 0;
|
||||||
|
virtual HRESULT GetReferenceStatus (/* out */ BMDReferenceStatus* referenceStatus) = 0;
|
||||||
|
|
||||||
|
/* Hardware Timing */
|
||||||
|
|
||||||
|
virtual HRESULT GetHardwareReferenceClock (/* in */ BMDTimeScale desiredTimeScale, /* out */ BMDTimeValue* hardwareTime, /* out */ BMDTimeValue* timeInFrame, /* out */ BMDTimeValue* ticksPerFrame) = 0;
|
||||||
|
virtual HRESULT GetFrameCompletionReferenceTimestamp (/* in */ IDeckLinkVideoFrame_v14_2_1* theFrame, /* in */ BMDTimeScale desiredTimeScale, /* out */ BMDTimeValue* frameCompletionTimestamp) = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual ~IDeckLinkOutput_v14_2_1 () {} // call Release method to drop reference count
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
103
services/capture/sdk/DeckLinkAPIVideoOutput_v15_3_1.h
Normal file
103
services/capture/sdk/DeckLinkAPIVideoOutput_v15_3_1.h
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
/* -LICENSE-START-
|
||||||
|
** Copyright (c) 2025 Blackmagic Design
|
||||||
|
**
|
||||||
|
** Permission is hereby granted, free of charge, to any person or organization
|
||||||
|
** obtaining a copy of the software and accompanying documentation (the
|
||||||
|
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||||
|
** and transmit the Software, and to prepare derivative works of the Software,
|
||||||
|
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||||
|
** accordance with:
|
||||||
|
**
|
||||||
|
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||||
|
** Agreement for the Software Development Kit (“EULA”) available at
|
||||||
|
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||||
|
**
|
||||||
|
** (2) if the Software is obtained from any third party, such licensing terms
|
||||||
|
** as notified by that third party,
|
||||||
|
**
|
||||||
|
** and all subject to the following:
|
||||||
|
**
|
||||||
|
** (3) the copyright notices in the Software and this entire statement,
|
||||||
|
** including the above license grant, this restriction and the following
|
||||||
|
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||||
|
** part, and all derivative works of the Software, unless such copies or
|
||||||
|
** derivative works are solely in the form of machine-executable object code
|
||||||
|
** generated by a source language processor.
|
||||||
|
**
|
||||||
|
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||||
|
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||||
|
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||||
|
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
** DEALINGS IN THE SOFTWARE.
|
||||||
|
**
|
||||||
|
** A copy of the Software is available free of charge at
|
||||||
|
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||||
|
**
|
||||||
|
** -LICENSE-END-
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "DeckLinkAPI_v15_3_1.h"
|
||||||
|
|
||||||
|
// Type Declarations
|
||||||
|
|
||||||
|
BMD_CONST REFIID IID_IDeckLinkOutput_v15_3_1 = /* 1A8077F1-9FE2-4533-8147-2294305E253F */ { 0x1A,0x80,0x77,0xF1,0x9F,0xE2,0x45,0x33,0x81,0x47,0x22,0x94,0x30,0x5E,0x25,0x3F };
|
||||||
|
|
||||||
|
#if defined(__cplusplus)
|
||||||
|
|
||||||
|
/* Interface IDeckLinkOutput - Created by QueryInterface from IDeckLink. */
|
||||||
|
|
||||||
|
class BMD_PUBLIC IDeckLinkOutput_v15_3_1 : public IUnknown
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual HRESULT DoesSupportVideoMode (/* in */ BMDVideoConnection connection /* If a value of bmdVideoConnectionUnspecified is specified, the caller does not care about the connection */, /* in */ BMDDisplayMode requestedMode, /* in */ BMDPixelFormat requestedPixelFormat, /* in */ BMDVideoOutputConversionMode conversionMode, /* in */ BMDSupportedVideoModeFlags flags, /* out */ BMDDisplayMode* actualMode, /* out */ bool* supported) = 0;
|
||||||
|
virtual HRESULT GetDisplayMode (/* in */ BMDDisplayMode displayMode, /* out */ IDeckLinkDisplayMode** resultDisplayMode) = 0;
|
||||||
|
virtual HRESULT GetDisplayModeIterator (/* out */ IDeckLinkDisplayModeIterator** iterator) = 0;
|
||||||
|
virtual HRESULT SetScreenPreviewCallback (/* in */ IDeckLinkScreenPreviewCallback* previewCallback) = 0;
|
||||||
|
|
||||||
|
/* Video Output */
|
||||||
|
|
||||||
|
virtual HRESULT EnableVideoOutput (/* in */ BMDDisplayMode displayMode, /* in */ BMDVideoOutputFlags flags) = 0;
|
||||||
|
virtual HRESULT DisableVideoOutput (void) = 0;
|
||||||
|
virtual HRESULT CreateVideoFrame (/* in */ int32_t width, /* in */ int32_t height, /* in */ int32_t rowBytes, /* in */ BMDPixelFormat pixelFormat, /* in */ BMDFrameFlags flags, /* out */ IDeckLinkMutableVideoFrame** outFrame) = 0;
|
||||||
|
virtual HRESULT CreateVideoFrameWithBuffer (/* in */ int32_t width, /* in */ int32_t height, /* in */ int32_t rowBytes, /* in */ BMDPixelFormat pixelFormat, /* in */ BMDFrameFlags flags, /* in */ IDeckLinkVideoBuffer_v15_3_1* buffer, /* out */ IDeckLinkMutableVideoFrame** outFrame) = 0;
|
||||||
|
virtual HRESULT RowBytesForPixelFormat (/* in */ BMDPixelFormat pixelFormat, /* in */ int32_t width, /* out */ int32_t* rowBytes) = 0;
|
||||||
|
virtual HRESULT CreateAncillaryData (/* in */ BMDPixelFormat pixelFormat, /* out */ IDeckLinkVideoFrameAncillary** outBuffer) = 0; // Use of IDeckLinkVideoFrameAncillaryPackets is preferred
|
||||||
|
virtual HRESULT DisplayVideoFrameSync (/* in */ IDeckLinkVideoFrame* theFrame) = 0;
|
||||||
|
virtual HRESULT ScheduleVideoFrame (/* in */ IDeckLinkVideoFrame* theFrame, /* in */ BMDTimeValue displayTime, /* in */ BMDTimeValue displayDuration, /* in */ BMDTimeScale timeScale) = 0;
|
||||||
|
virtual HRESULT SetScheduledFrameCompletionCallback (/* in */ IDeckLinkVideoOutputCallback* theCallback) = 0;
|
||||||
|
virtual HRESULT GetBufferedVideoFrameCount (/* out */ uint32_t* bufferedFrameCount) = 0;
|
||||||
|
|
||||||
|
/* Audio Output */
|
||||||
|
|
||||||
|
virtual HRESULT EnableAudioOutput (/* in */ BMDAudioSampleRate sampleRate, /* in */ BMDAudioSampleType sampleType, /* in */ uint32_t channelCount, /* in */ BMDAudioOutputStreamType streamType) = 0;
|
||||||
|
virtual HRESULT DisableAudioOutput (void) = 0;
|
||||||
|
virtual HRESULT WriteAudioSamplesSync (/* in */ void* buffer, /* in */ uint32_t sampleFrameCount, /* out */ uint32_t* sampleFramesWritten) = 0;
|
||||||
|
virtual HRESULT BeginAudioPreroll (void) = 0;
|
||||||
|
virtual HRESULT EndAudioPreroll (void) = 0;
|
||||||
|
virtual HRESULT ScheduleAudioSamples (/* in */ void* buffer, /* in */ uint32_t sampleFrameCount, /* in */ BMDTimeValue streamTime, /* in */ BMDTimeScale timeScale, /* out */ uint32_t* sampleFramesWritten) = 0;
|
||||||
|
virtual HRESULT GetBufferedAudioSampleFrameCount (/* out */ uint32_t* bufferedSampleFrameCount) = 0;
|
||||||
|
virtual HRESULT FlushBufferedAudioSamples (void) = 0;
|
||||||
|
virtual HRESULT SetAudioCallback (/* in */ IDeckLinkAudioOutputCallback* theCallback) = 0;
|
||||||
|
|
||||||
|
/* Output Control */
|
||||||
|
|
||||||
|
virtual HRESULT StartScheduledPlayback (/* in */ BMDTimeValue playbackStartTime, /* in */ BMDTimeScale timeScale, /* in */ double playbackSpeed) = 0;
|
||||||
|
virtual HRESULT StopScheduledPlayback (/* in */ BMDTimeValue stopPlaybackAtTime, /* out */ BMDTimeValue* actualStopTime, /* in */ BMDTimeScale timeScale) = 0;
|
||||||
|
virtual HRESULT IsScheduledPlaybackRunning (/* out */ bool* active) = 0;
|
||||||
|
virtual HRESULT GetScheduledStreamTime (/* in */ BMDTimeScale desiredTimeScale, /* out */ BMDTimeValue* streamTime, /* out */ double* playbackSpeed) = 0;
|
||||||
|
virtual HRESULT GetReferenceStatus (/* out */ BMDReferenceStatus* referenceStatus) = 0;
|
||||||
|
|
||||||
|
/* Hardware Timing */
|
||||||
|
|
||||||
|
virtual HRESULT GetHardwareReferenceClock (/* in */ BMDTimeScale desiredTimeScale, /* out */ BMDTimeValue* hardwareTime, /* out */ BMDTimeValue* timeInFrame, /* out */ BMDTimeValue* ticksPerFrame) = 0;
|
||||||
|
virtual HRESULT GetFrameCompletionReferenceTimestamp (/* in */ IDeckLinkVideoFrame* theFrame, /* in */ BMDTimeScale desiredTimeScale, /* out */ BMDTimeValue* frameCompletionTimestamp) = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual ~IDeckLinkOutput_v15_3_1 () {} // call Release method to drop reference count
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // defined(__cplusplus)
|
||||||
134
services/capture/sdk/DeckLinkAPI_v10_11.h
Normal file
134
services/capture/sdk/DeckLinkAPI_v10_11.h
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
/* -LICENSE-START-
|
||||||
|
** Copyright (c) 2018 Blackmagic Design
|
||||||
|
**
|
||||||
|
** Permission is hereby granted, free of charge, to any person or organization
|
||||||
|
** obtaining a copy of the software and accompanying documentation (the
|
||||||
|
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||||
|
** and transmit the Software, and to prepare derivative works of the Software,
|
||||||
|
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||||
|
** accordance with:
|
||||||
|
**
|
||||||
|
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||||
|
** Agreement for the Software Development Kit ("EULA") available at
|
||||||
|
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||||
|
**
|
||||||
|
** (2) if the Software is obtained from any third party, such licensing terms
|
||||||
|
** as notified by that third party,
|
||||||
|
**
|
||||||
|
** and all subject to the following:
|
||||||
|
**
|
||||||
|
** (3) the copyright notices in the Software and this entire statement,
|
||||||
|
** including the above license grant, this restriction and the following
|
||||||
|
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||||
|
** part, and all derivative works of the Software, unless such copies or
|
||||||
|
** derivative works are solely in the form of machine-executable object code
|
||||||
|
** generated by a source language processor.
|
||||||
|
**
|
||||||
|
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||||
|
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||||
|
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||||
|
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
** DEALINGS IN THE SOFTWARE.
|
||||||
|
**
|
||||||
|
** A copy of the Software is available free of charge at
|
||||||
|
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||||
|
**
|
||||||
|
** -LICENSE-END-
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef BMD_DECKLINKAPI_v10_11_H
|
||||||
|
#define BMD_DECKLINKAPI_v10_11_H
|
||||||
|
|
||||||
|
#include "DeckLinkAPI.h"
|
||||||
|
|
||||||
|
// Interface ID Declarations
|
||||||
|
|
||||||
|
BMD_CONST REFIID IID_IDeckLinkAttributes_v10_11 = /* ABC11843-D966-44CB-96E2-A1CB5D3135C4 */ {0xAB,0xC1,0x18,0x43,0xD9,0x66,0x44,0xCB,0x96,0xE2,0xA1,0xCB,0x5D,0x31,0x35,0xC4};
|
||||||
|
BMD_CONST REFIID IID_IDeckLinkNotification_v10_11 = /* 0A1FB207-E215-441B-9B19-6FA1575946C5 */ {0x0A,0x1F,0xB2,0x07,0xE2,0x15,0x44,0x1B,0x9B,0x19,0x6F,0xA1,0x57,0x59,0x46,0xC5};
|
||||||
|
|
||||||
|
/* Enum BMDDisplayModeSupport_v10_11 - Output mode supported flags */
|
||||||
|
|
||||||
|
typedef uint32_t BMDDisplayModeSupport_v10_11;
|
||||||
|
enum _BMDDisplayModeSupport_v10_11 {
|
||||||
|
bmdDisplayModeNotSupported_v10_11 = 0,
|
||||||
|
bmdDisplayModeSupported_v10_11,
|
||||||
|
bmdDisplayModeSupportedWithConversion_v10_11
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Enum BMDDuplexMode_v10_11 - Duplex for configurable ports */
|
||||||
|
|
||||||
|
typedef uint32_t BMDDuplexMode_v10_11;
|
||||||
|
enum _BMDDuplexMode_v10_11 {
|
||||||
|
bmdDuplexModeFull_v10_11 = /* 'fdup' */ 0x66647570,
|
||||||
|
bmdDuplexModeHalf_v10_11 = /* 'hdup' */ 0x68647570
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Enum BMDDeckLinkAttributeID_v10_11 - DeckLink Attribute ID */
|
||||||
|
|
||||||
|
enum _BMDDeckLinkAttributeID_v10_11 {
|
||||||
|
|
||||||
|
/* Flags */
|
||||||
|
|
||||||
|
BMDDeckLinkSupportsDuplexModeConfiguration_v10_11 = 'dupx',
|
||||||
|
BMDDeckLinkSupportsHDKeying_v10_11 = 'keyh',
|
||||||
|
|
||||||
|
/* Integers */
|
||||||
|
|
||||||
|
BMDDeckLinkPairedDevicePersistentID_v10_11 = 'ppid',
|
||||||
|
BMDDeckLinkSupportsFullDuplex_v10_11 = 'fdup',
|
||||||
|
};
|
||||||
|
|
||||||
|
enum _BMDDeckLinkStatusID_v10_11 {
|
||||||
|
bmdDeckLinkStatusDuplexMode_v10_11 = 'dupx',
|
||||||
|
};
|
||||||
|
|
||||||
|
typedef uint32_t BMDDuplexStatus_v10_11;
|
||||||
|
enum _BMDDuplexStatus_v10_11 {
|
||||||
|
bmdDuplexFullDuplex_v10_11 = 'fdup',
|
||||||
|
bmdDuplexHalfDuplex_v10_11 = 'hdup',
|
||||||
|
bmdDuplexSimplex_v10_11 = 'splx',
|
||||||
|
bmdDuplexInactive_v10_11 = 'inac',
|
||||||
|
};
|
||||||
|
|
||||||
|
#if defined(__cplusplus)
|
||||||
|
|
||||||
|
/* Interface IDeckLinkAttributes_v10_11 - DeckLink Attribute interface */
|
||||||
|
|
||||||
|
class BMD_PUBLIC IDeckLinkAttributes_v10_11 : public IUnknown
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual HRESULT GetFlag (/* in */ BMDDeckLinkAttributeID cfgID, /* out */ bool *value) = 0;
|
||||||
|
virtual HRESULT GetInt (/* in */ BMDDeckLinkAttributeID cfgID, /* out */ int64_t *value) = 0;
|
||||||
|
virtual HRESULT GetFloat (/* in */ BMDDeckLinkAttributeID cfgID, /* out */ double *value) = 0;
|
||||||
|
virtual HRESULT GetString (/* in */ BMDDeckLinkAttributeID cfgID, /* out */ const char **value) = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual ~IDeckLinkAttributes_v10_11 () {} // call Release method to drop reference count
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Interface IDeckLinkNotification_v10_11 - DeckLink Notification interface */
|
||||||
|
|
||||||
|
class BMD_PUBLIC IDeckLinkNotification_v10_11 : public IUnknown
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual HRESULT Subscribe (/* in */ BMDNotifications topic, /* in */ IDeckLinkNotificationCallback *theCallback) = 0;
|
||||||
|
virtual HRESULT Unsubscribe (/* in */ BMDNotifications topic, /* in */ IDeckLinkNotificationCallback *theCallback) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Functions */
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
BMD_PUBLIC IDeckLinkIterator* CreateDeckLinkIteratorInstance_v10_11 (void);
|
||||||
|
BMD_PUBLIC IDeckLinkDiscovery* CreateDeckLinkDiscoveryInstance_v10_11 (void);
|
||||||
|
BMD_PUBLIC IDeckLinkAPIInformation* CreateDeckLinkAPIInformationInstance_v10_11 (void);
|
||||||
|
BMD_PUBLIC IDeckLinkGLScreenPreviewHelper* CreateOpenGLScreenPreviewHelper_v10_11 (void);
|
||||||
|
BMD_PUBLIC IDeckLinkVideoConversion* CreateVideoConversionInstance_v10_11 (void);
|
||||||
|
BMD_PUBLIC IDeckLinkVideoFrameAncillaryPackets* CreateVideoFrameAncillaryPacketsInstance_v10_11 (void); // For use when creating a custom IDeckLinkVideoFrame without wrapping IDeckLinkOutput::CreateVideoFrame
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // defined(__cplusplus)
|
||||||
|
#endif /* defined(BMD_DECKLINKAPI_v10_11_H) */
|
||||||
68
services/capture/sdk/DeckLinkAPI_v10_2.h
Normal file
68
services/capture/sdk/DeckLinkAPI_v10_2.h
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
/* -LICENSE-START-
|
||||||
|
** Copyright (c) 2014 Blackmagic Design
|
||||||
|
**
|
||||||
|
** Permission is hereby granted, free of charge, to any person or organization
|
||||||
|
** obtaining a copy of the software and accompanying documentation (the
|
||||||
|
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||||
|
** and transmit the Software, and to prepare derivative works of the Software,
|
||||||
|
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||||
|
** accordance with:
|
||||||
|
**
|
||||||
|
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||||
|
** Agreement for the Software Development Kit ("EULA") available at
|
||||||
|
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||||
|
**
|
||||||
|
** (2) if the Software is obtained from any third party, such licensing terms
|
||||||
|
** as notified by that third party,
|
||||||
|
**
|
||||||
|
** and all subject to the following:
|
||||||
|
**
|
||||||
|
** (3) the copyright notices in the Software and this entire statement,
|
||||||
|
** including the above license grant, this restriction and the following
|
||||||
|
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||||
|
** part, and all derivative works of the Software, unless such copies or
|
||||||
|
** derivative works are solely in the form of machine-executable object code
|
||||||
|
** generated by a source language processor.
|
||||||
|
**
|
||||||
|
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||||
|
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||||
|
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||||
|
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
** DEALINGS IN THE SOFTWARE.
|
||||||
|
**
|
||||||
|
** A copy of the Software is available free of charge at
|
||||||
|
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||||
|
**
|
||||||
|
** -LICENSE-END-
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef BMD_DECKLINKAPI_v10_2_H
|
||||||
|
#define BMD_DECKLINKAPI_v10_2_H
|
||||||
|
|
||||||
|
#include "DeckLinkAPI.h"
|
||||||
|
|
||||||
|
// Type Declarations
|
||||||
|
|
||||||
|
/* Enum BMDDeckLinkConfigurationID - DeckLink Configuration ID */
|
||||||
|
|
||||||
|
typedef uint32_t BMDDeckLinkConfigurationID_v10_2;
|
||||||
|
enum _BMDDeckLinkConfigurationID_v10_2 {
|
||||||
|
/* Video output flags */
|
||||||
|
|
||||||
|
bmdDeckLinkConfig3GBpsVideoOutput_v10_2 = '3gbs',
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Enum BMDAudioConnection_v10_2 - Audio connection types */
|
||||||
|
|
||||||
|
typedef uint32_t BMDAudioConnection_v10_2;
|
||||||
|
enum _BMDAudioConnection_v10_2 {
|
||||||
|
bmdAudioConnectionEmbedded_v10_2 = /* 'embd' */ 0x656D6264,
|
||||||
|
bmdAudioConnectionAESEBU_v10_2 = /* 'aes ' */ 0x61657320,
|
||||||
|
bmdAudioConnectionAnalog_v10_2 = /* 'anlg' */ 0x616E6C67,
|
||||||
|
bmdAudioConnectionAnalogXLR_v10_2 = /* 'axlr' */ 0x61786C72,
|
||||||
|
bmdAudioConnectionAnalogRCA_v10_2 = /* 'arca' */ 0x61726361
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /* defined(BMD_DECKLINKAPI_v10_2_H) */
|
||||||
58
services/capture/sdk/DeckLinkAPI_v10_4.h
Normal file
58
services/capture/sdk/DeckLinkAPI_v10_4.h
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
/* -LICENSE-START-
|
||||||
|
** Copyright (c) 2015 Blackmagic Design
|
||||||
|
**
|
||||||
|
** Permission is hereby granted, free of charge, to any person or organization
|
||||||
|
** obtaining a copy of the software and accompanying documentation (the
|
||||||
|
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||||
|
** and transmit the Software, and to prepare derivative works of the Software,
|
||||||
|
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||||
|
** accordance with:
|
||||||
|
**
|
||||||
|
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||||
|
** Agreement for the Software Development Kit ("EULA") available at
|
||||||
|
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||||
|
**
|
||||||
|
** (2) if the Software is obtained from any third party, such licensing terms
|
||||||
|
** as notified by that third party,
|
||||||
|
**
|
||||||
|
** and all subject to the following:
|
||||||
|
**
|
||||||
|
** (3) the copyright notices in the Software and this entire statement,
|
||||||
|
** including the above license grant, this restriction and the following
|
||||||
|
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||||
|
** part, and all derivative works of the Software, unless such copies or
|
||||||
|
** derivative works are solely in the form of machine-executable object code
|
||||||
|
** generated by a source language processor.
|
||||||
|
**
|
||||||
|
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||||
|
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||||
|
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||||
|
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
** DEALINGS IN THE SOFTWARE.
|
||||||
|
**
|
||||||
|
** A copy of the Software is available free of charge at
|
||||||
|
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||||
|
**
|
||||||
|
** -LICENSE-END-
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef BMD_DECKLINKAPI_v10_4_H
|
||||||
|
#define BMD_DECKLINKAPI_v10_4_H
|
||||||
|
|
||||||
|
#include "DeckLinkAPI.h"
|
||||||
|
|
||||||
|
// Type Declarations
|
||||||
|
|
||||||
|
/* Enum BMDDeckLinkConfigurationID - DeckLink Configuration ID */
|
||||||
|
|
||||||
|
typedef uint32_t BMDDeckLinkConfigurationID_v10_4;
|
||||||
|
enum _BMDDeckLinkConfigurationID_v10_4 {
|
||||||
|
|
||||||
|
/* Video output flags */
|
||||||
|
|
||||||
|
bmdDeckLinkConfigSingleLinkVideoOutput_v10_4 = /* 'sglo' */ 0x73676C6F,
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /* defined(BMD_DECKLINKAPI_v10_4_H) */
|
||||||
59
services/capture/sdk/DeckLinkAPI_v10_5.h
Normal file
59
services/capture/sdk/DeckLinkAPI_v10_5.h
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
/* -LICENSE-START-
|
||||||
|
** Copyright (c) 2015 Blackmagic Design
|
||||||
|
**
|
||||||
|
** Permission is hereby granted, free of charge, to any person or organization
|
||||||
|
** obtaining a copy of the software and accompanying documentation (the
|
||||||
|
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||||
|
** and transmit the Software, and to prepare derivative works of the Software,
|
||||||
|
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||||
|
** accordance with:
|
||||||
|
**
|
||||||
|
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||||
|
** Agreement for the Software Development Kit ("EULA") available at
|
||||||
|
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||||
|
**
|
||||||
|
** (2) if the Software is obtained from any third party, such licensing terms
|
||||||
|
** as notified by that third party,
|
||||||
|
**
|
||||||
|
** and all subject to the following:
|
||||||
|
**
|
||||||
|
** (3) the copyright notices in the Software and this entire statement,
|
||||||
|
** including the above license grant, this restriction and the following
|
||||||
|
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||||
|
** part, and all derivative works of the Software, unless such copies or
|
||||||
|
** derivative works are solely in the form of machine-executable object code
|
||||||
|
** generated by a source language processor.
|
||||||
|
**
|
||||||
|
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||||
|
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||||
|
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||||
|
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
** DEALINGS IN THE SOFTWARE.
|
||||||
|
**
|
||||||
|
** A copy of the Software is available free of charge at
|
||||||
|
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||||
|
**
|
||||||
|
** -LICENSE-END-
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef BMD_DECKLINKAPI_v10_5_H
|
||||||
|
#define BMD_DECKLINKAPI_v10_5_H
|
||||||
|
|
||||||
|
#include "DeckLinkAPI.h"
|
||||||
|
|
||||||
|
// Type Declarations
|
||||||
|
|
||||||
|
/* Enum BMDDeckLinkAttributeID - DeckLink Attribute ID */
|
||||||
|
|
||||||
|
typedef uint32_t BMDDeckLinkAttributeID_v10_5;
|
||||||
|
enum _BMDDeckLinkAttributeID_v10_5 {
|
||||||
|
|
||||||
|
/* Integers */
|
||||||
|
|
||||||
|
BMDDeckLinkDeviceBusyState_v10_5 = /* 'dbst' */ 0x64627374,
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /* defined(BMD_DECKLINKAPI_v10_5_H) */
|
||||||
|
|
||||||
63
services/capture/sdk/DeckLinkAPI_v10_6.h
Normal file
63
services/capture/sdk/DeckLinkAPI_v10_6.h
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
/* -LICENSE-START-
|
||||||
|
** Copyright (c) 2016 Blackmagic Design
|
||||||
|
**
|
||||||
|
** Permission is hereby granted, free of charge, to any person or organization
|
||||||
|
** obtaining a copy of the software and accompanying documentation (the
|
||||||
|
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||||
|
** and transmit the Software, and to prepare derivative works of the Software,
|
||||||
|
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||||
|
** accordance with:
|
||||||
|
**
|
||||||
|
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||||
|
** Agreement for the Software Development Kit ("EULA") available at
|
||||||
|
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||||
|
**
|
||||||
|
** (2) if the Software is obtained from any third party, such licensing terms
|
||||||
|
** as notified by that third party,
|
||||||
|
**
|
||||||
|
** and all subject to the following:
|
||||||
|
**
|
||||||
|
** (3) the copyright notices in the Software and this entire statement,
|
||||||
|
** including the above license grant, this restriction and the following
|
||||||
|
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||||
|
** part, and all derivative works of the Software, unless such copies or
|
||||||
|
** derivative works are solely in the form of machine-executable object code
|
||||||
|
** generated by a source language processor.
|
||||||
|
**
|
||||||
|
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||||
|
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||||
|
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||||
|
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
** DEALINGS IN THE SOFTWARE.
|
||||||
|
**
|
||||||
|
** A copy of the Software is available free of charge at
|
||||||
|
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||||
|
**
|
||||||
|
** -LICENSE-END-
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef BMD_DECKLINKAPI_v10_6_H
|
||||||
|
#define BMD_DECKLINKAPI_v10_6_H
|
||||||
|
|
||||||
|
#include "DeckLinkAPI.h"
|
||||||
|
|
||||||
|
// Type Declarations
|
||||||
|
|
||||||
|
/* Enum BMDDeckLinkAttributeID - DeckLink Attribute ID */
|
||||||
|
|
||||||
|
typedef uint32_t BMDDeckLinkAttributeID_c10_6;
|
||||||
|
enum _BMDDeckLinkAttributeID_v10_6 {
|
||||||
|
|
||||||
|
/* Flags */
|
||||||
|
|
||||||
|
BMDDeckLinkSupportsDesktopDisplay_v10_6 = /* 'extd' */ 0x65787464,
|
||||||
|
};
|
||||||
|
|
||||||
|
typedef uint32_t BMDIdleVideoOutputOperation_v10_6;
|
||||||
|
enum _BMDIdleVideoOutputOperation_v10_6 {
|
||||||
|
bmdIdleVideoOutputDesktop_v10_6 = /* 'desk' */ 0x6465736B
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /* defined(BMD_DECKLINKAPI_v10_6_H) */
|
||||||
58
services/capture/sdk/DeckLinkAPI_v10_9.h
Normal file
58
services/capture/sdk/DeckLinkAPI_v10_9.h
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
/* -LICENSE-START-
|
||||||
|
** Copyright (c) 2017 Blackmagic Design
|
||||||
|
**
|
||||||
|
** Permission is hereby granted, free of charge, to any person or organization
|
||||||
|
** obtaining a copy of the software and accompanying documentation (the
|
||||||
|
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||||
|
** and transmit the Software, and to prepare derivative works of the Software,
|
||||||
|
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||||
|
** accordance with:
|
||||||
|
**
|
||||||
|
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||||
|
** Agreement for the Software Development Kit ("EULA") available at
|
||||||
|
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||||
|
**
|
||||||
|
** (2) if the Software is obtained from any third party, such licensing terms
|
||||||
|
** as notified by that third party,
|
||||||
|
**
|
||||||
|
** and all subject to the following:
|
||||||
|
**
|
||||||
|
** (3) the copyright notices in the Software and this entire statement,
|
||||||
|
** including the above license grant, this restriction and the following
|
||||||
|
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||||
|
** part, and all derivative works of the Software, unless such copies or
|
||||||
|
** derivative works are solely in the form of machine-executable object code
|
||||||
|
** generated by a source language processor.
|
||||||
|
**
|
||||||
|
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||||
|
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||||
|
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||||
|
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
** DEALINGS IN THE SOFTWARE.
|
||||||
|
**
|
||||||
|
** A copy of the Software is available free of charge at
|
||||||
|
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||||
|
**
|
||||||
|
** -LICENSE-END-
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef BMD_DECKLINKAPI_v10_9_H
|
||||||
|
#define BMD_DECKLINKAPI_v10_9_H
|
||||||
|
|
||||||
|
#include "DeckLinkAPI.h"
|
||||||
|
|
||||||
|
// Type Declarations
|
||||||
|
|
||||||
|
/* Enum BMDDeckLinkAttributeID - DeckLink Attribute ID */
|
||||||
|
|
||||||
|
typedef uint32_t BMDDeckLinkConfigurationID_v10_9;
|
||||||
|
enum _BMDDeckLinkConfigurationID_v10_9 {
|
||||||
|
|
||||||
|
/* Flags */
|
||||||
|
|
||||||
|
bmdDeckLinkConfig1080pNotPsF_v10_9 = 'fpro',
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /* defined(BMD_DECKLINKAPI_v10_9_H) */
|
||||||
113
services/capture/sdk/DeckLinkAPI_v11_5.h
Normal file
113
services/capture/sdk/DeckLinkAPI_v11_5.h
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
/* -LICENSE-START-
|
||||||
|
** Copyright (c) 2020 Blackmagic Design
|
||||||
|
**
|
||||||
|
** Permission is hereby granted, free of charge, to any person or organization
|
||||||
|
** obtaining a copy of the software and accompanying documentation (the
|
||||||
|
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||||
|
** and transmit the Software, and to prepare derivative works of the Software,
|
||||||
|
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||||
|
** accordance with:
|
||||||
|
**
|
||||||
|
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||||
|
** Agreement for the Software Development Kit ("EULA") available at
|
||||||
|
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||||
|
**
|
||||||
|
** (2) if the Software is obtained from any third party, such licensing terms
|
||||||
|
** as notified by that third party,
|
||||||
|
**
|
||||||
|
** and all subject to the following:
|
||||||
|
**
|
||||||
|
** (3) the copyright notices in the Software and this entire statement,
|
||||||
|
** including the above license grant, this restriction and the following
|
||||||
|
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||||
|
** part, and all derivative works of the Software, unless such copies or
|
||||||
|
** derivative works are solely in the form of machine-executable object code
|
||||||
|
** generated by a source language processor.
|
||||||
|
**
|
||||||
|
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||||
|
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||||
|
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||||
|
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
** DEALINGS IN THE SOFTWARE.
|
||||||
|
**
|
||||||
|
** A copy of the Software is available free of charge at
|
||||||
|
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||||
|
**
|
||||||
|
** -LICENSE-END-
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef BMD_DECKLINKAPI_v11_5_H
|
||||||
|
#define BMD_DECKLINKAPI_v11_5_H
|
||||||
|
|
||||||
|
#include "DeckLinkAPI.h"
|
||||||
|
|
||||||
|
BMD_CONST REFIID IID_IDeckLinkVideoFrameMetadataExtensions_v11_5 = /* D5973DC9-6432-46D0-8F0B-2496F8A1238F */ {0xD5,0x97,0x3D,0xC9,0x64,0x32,0x46,0xD0,0x8F,0x0B,0x24,0x96,0xF8,0xA1,0x23,0x8F};
|
||||||
|
|
||||||
|
/* Enum BMDDeckLinkFrameMetadataID - DeckLink Frame Metadata ID */
|
||||||
|
|
||||||
|
typedef uint32_t BMDDeckLinkFrameMetadataID_v11_5;
|
||||||
|
enum _BMDDeckLinkFrameMetadataID_v11_5 {
|
||||||
|
bmdDeckLinkFrameMetadataCintelFilmType_v11_5 = /* 'cfty' */ 0x63667479, // Current film type
|
||||||
|
bmdDeckLinkFrameMetadataCintelFilmGauge_v11_5 = /* 'cfga' */ 0x63666761, // Current film gauge
|
||||||
|
bmdDeckLinkFrameMetadataCintelKeykodeLow_v11_5 = /* 'ckkl' */ 0x636B6B6C, // Raw keykode value - low 64 bits
|
||||||
|
bmdDeckLinkFrameMetadataCintelKeykodeHigh_v11_5 = /* 'ckkh' */ 0x636B6B68, // Raw keykode value - high 64 bits
|
||||||
|
bmdDeckLinkFrameMetadataCintelTile1Size_v11_5 = /* 'ct1s' */ 0x63743173, // Size in bytes of compressed raw tile 1
|
||||||
|
bmdDeckLinkFrameMetadataCintelTile2Size_v11_5 = /* 'ct2s' */ 0x63743273, // Size in bytes of compressed raw tile 2
|
||||||
|
bmdDeckLinkFrameMetadataCintelTile3Size_v11_5 = /* 'ct3s' */ 0x63743373, // Size in bytes of compressed raw tile 3
|
||||||
|
bmdDeckLinkFrameMetadataCintelTile4Size_v11_5 = /* 'ct4s' */ 0x63743473, // Size in bytes of compressed raw tile 4
|
||||||
|
bmdDeckLinkFrameMetadataCintelImageWidth_v11_5 = /* 'IWPx' */ 0x49575078, // Width in pixels of image
|
||||||
|
bmdDeckLinkFrameMetadataCintelImageHeight_v11_5 = /* 'IHPx' */ 0x49485078, // Height in pixels of image
|
||||||
|
bmdDeckLinkFrameMetadataCintelLinearMaskingRedInRed_v11_5 = /* 'mrir' */ 0x6D726972, // Red in red linear masking parameter
|
||||||
|
bmdDeckLinkFrameMetadataCintelLinearMaskingGreenInRed_v11_5 = /* 'mgir' */ 0x6D676972, // Green in red linear masking parameter
|
||||||
|
bmdDeckLinkFrameMetadataCintelLinearMaskingBlueInRed_v11_5 = /* 'mbir' */ 0x6D626972, // Blue in red linear masking parameter
|
||||||
|
bmdDeckLinkFrameMetadataCintelLinearMaskingRedInGreen_v11_5 = /* 'mrig' */ 0x6D726967, // Red in green linear masking parameter
|
||||||
|
bmdDeckLinkFrameMetadataCintelLinearMaskingGreenInGreen_v11_5 = /* 'mgig' */ 0x6D676967, // Green in green linear masking parameter
|
||||||
|
bmdDeckLinkFrameMetadataCintelLinearMaskingBlueInGreen_v11_5 = /* 'mbig' */ 0x6D626967, // Blue in green linear masking parameter
|
||||||
|
bmdDeckLinkFrameMetadataCintelLinearMaskingRedInBlue_v11_5 = /* 'mrib' */ 0x6D726962, // Red in blue linear masking parameter
|
||||||
|
bmdDeckLinkFrameMetadataCintelLinearMaskingGreenInBlue_v11_5 = /* 'mgib' */ 0x6D676962, // Green in blue linear masking parameter
|
||||||
|
bmdDeckLinkFrameMetadataCintelLinearMaskingBlueInBlue_v11_5 = /* 'mbib' */ 0x6D626962, // Blue in blue linear masking parameter
|
||||||
|
bmdDeckLinkFrameMetadataCintelLogMaskingRedInRed_v11_5 = /* 'mlrr' */ 0x6D6C7272, // Red in red log masking parameter
|
||||||
|
bmdDeckLinkFrameMetadataCintelLogMaskingGreenInRed_v11_5 = /* 'mlgr' */ 0x6D6C6772, // Green in red log masking parameter
|
||||||
|
bmdDeckLinkFrameMetadataCintelLogMaskingBlueInRed_v11_5 = /* 'mlbr' */ 0x6D6C6272, // Blue in red log masking parameter
|
||||||
|
bmdDeckLinkFrameMetadataCintelLogMaskingRedInGreen_v11_5 = /* 'mlrg' */ 0x6D6C7267, // Red in green log masking parameter
|
||||||
|
bmdDeckLinkFrameMetadataCintelLogMaskingGreenInGreen_v11_5 = /* 'mlgg' */ 0x6D6C6767, // Green in green log masking parameter
|
||||||
|
bmdDeckLinkFrameMetadataCintelLogMaskingBlueInGreen_v11_5 = /* 'mlbg' */ 0x6D6C6267, // Blue in green log masking parameter
|
||||||
|
bmdDeckLinkFrameMetadataCintelLogMaskingRedInBlue_v11_5 = /* 'mlrb' */ 0x6D6C7262, // Red in blue log masking parameter
|
||||||
|
bmdDeckLinkFrameMetadataCintelLogMaskingGreenInBlue_v11_5 = /* 'mlgb' */ 0x6D6C6762, // Green in blue log masking parameter
|
||||||
|
bmdDeckLinkFrameMetadataCintelLogMaskingBlueInBlue_v11_5 = /* 'mlbb' */ 0x6D6C6262, // Blue in blue log masking parameter
|
||||||
|
bmdDeckLinkFrameMetadataCintelFilmFrameRate_v11_5 = /* 'cffr' */ 0x63666672, // Film frame rate
|
||||||
|
bmdDeckLinkFrameMetadataCintelOffsetToApplyHorizontal_v11_5 = /* 'otah' */ 0x6F746168, // Horizontal offset (pixels) to be applied to image
|
||||||
|
bmdDeckLinkFrameMetadataCintelOffsetToApplyVertical_v11_5 = /* 'otav' */ 0x6F746176, // Vertical offset (pixels) to be applied to image
|
||||||
|
bmdDeckLinkFrameMetadataCintelGainRed_v11_5 = /* 'LfRd' */ 0x4C665264, // Red gain parameter to apply after log
|
||||||
|
bmdDeckLinkFrameMetadataCintelGainGreen_v11_5 = /* 'LfGr' */ 0x4C664772, // Green gain parameter to apply after log
|
||||||
|
bmdDeckLinkFrameMetadataCintelGainBlue_v11_5 = /* 'LfBl' */ 0x4C66426C, // Blue gain parameter to apply after log
|
||||||
|
bmdDeckLinkFrameMetadataCintelLiftRed_v11_5 = /* 'GnRd' */ 0x476E5264, // Red lift parameter to apply after log and gain
|
||||||
|
bmdDeckLinkFrameMetadataCintelLiftGreen_v11_5 = /* 'GnGr' */ 0x476E4772, // Green lift parameter to apply after log and gain
|
||||||
|
bmdDeckLinkFrameMetadataCintelLiftBlue_v11_5 = /* 'GnBl' */ 0x476E426C, // Blue lift parameter to apply after log and gain
|
||||||
|
bmdDeckLinkFrameMetadataCintelHDRGainRed_v11_5 = /* 'HGRd' */ 0x48475264, // Red gain parameter to apply to linear data for HDR Combination
|
||||||
|
bmdDeckLinkFrameMetadataCintelHDRGainGreen_v11_5 = /* 'HGGr' */ 0x48474772, // Green gain parameter to apply to linear data for HDR Combination
|
||||||
|
bmdDeckLinkFrameMetadataCintelHDRGainBlue_v11_5 = /* 'HGBl' */ 0x4847426C, // Blue gain parameter to apply to linear data for HDR Combination
|
||||||
|
bmdDeckLinkFrameMetadataCintel16mmCropRequired_v11_5 = /* 'c16c' */ 0x63313663, // The image should be cropped to 16mm size
|
||||||
|
bmdDeckLinkFrameMetadataCintelInversionRequired_v11_5 = /* 'cinv' */ 0x63696E76, // The image should be colour inverted
|
||||||
|
bmdDeckLinkFrameMetadataCintelFlipRequired_v11_5 = /* 'cflr' */ 0x63666C72, // The image should be flipped horizontally
|
||||||
|
bmdDeckLinkFrameMetadataCintelFocusAssistEnabled_v11_5 = /* 'cfae' */ 0x63666165, // Focus Assist is currently enabled
|
||||||
|
bmdDeckLinkFrameMetadataCintelKeykodeIsInterpolated_v11_5 = /* 'kkii' */ 0x6B6B6969 // The keykode for this frame is interpolated from nearby keykodes
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Interface IDeckLinkVideoFrameMetadataExtensions - Optional interface implemented on IDeckLinkVideoFrame to support frame metadata such as HDMI HDR information */
|
||||||
|
|
||||||
|
class BMD_PUBLIC IDeckLinkVideoFrameMetadataExtensions_v11_5 : public IUnknown
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual HRESULT GetInt (/* in */ BMDDeckLinkFrameMetadataID_v11_5 metadataID, /* out */ int64_t *value) = 0;
|
||||||
|
virtual HRESULT GetFloat (/* in */ BMDDeckLinkFrameMetadataID_v11_5 metadataID, /* out */ double *value) = 0;
|
||||||
|
virtual HRESULT GetFlag (/* in */ BMDDeckLinkFrameMetadataID_v11_5 metadataID, /* out */ bool* value) = 0;
|
||||||
|
virtual HRESULT GetString (/* in */ BMDDeckLinkFrameMetadataID_v11_5 metadataID, /* out */ const char **value) = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual ~IDeckLinkVideoFrameMetadataExtensions_v11_5 () {} // call Release method to drop reference count
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /* defined(BMD_DECKLINKAPI_v11_5_H) */
|
||||||
57
services/capture/sdk/DeckLinkAPI_v11_5_1.h
Normal file
57
services/capture/sdk/DeckLinkAPI_v11_5_1.h
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
/* -LICENSE-START-
|
||||||
|
** Copyright (c) 2020 Blackmagic Design
|
||||||
|
**
|
||||||
|
** Permission is hereby granted, free of charge, to any person or organization
|
||||||
|
** obtaining a copy of the software and accompanying documentation (the
|
||||||
|
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||||
|
** and transmit the Software, and to prepare derivative works of the Software,
|
||||||
|
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||||
|
** accordance with:
|
||||||
|
**
|
||||||
|
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||||
|
** Agreement for the Software Development Kit ("EULA") available at
|
||||||
|
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||||
|
**
|
||||||
|
** (2) if the Software is obtained from any third party, such licensing terms
|
||||||
|
** as notified by that third party,
|
||||||
|
**
|
||||||
|
** and all subject to the following:
|
||||||
|
**
|
||||||
|
** (3) the copyright notices in the Software and this entire statement,
|
||||||
|
** including the above license grant, this restriction and the following
|
||||||
|
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||||
|
** part, and all derivative works of the Software, unless such copies or
|
||||||
|
** derivative works are solely in the form of machine-executable object code
|
||||||
|
** generated by a source language processor.
|
||||||
|
**
|
||||||
|
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||||
|
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||||
|
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||||
|
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
** DEALINGS IN THE SOFTWARE.
|
||||||
|
**
|
||||||
|
** A copy of the Software is available free of charge at
|
||||||
|
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||||
|
**
|
||||||
|
** -LICENSE-END-
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef BMD_DECKLINKAPI_v11_5_1_H
|
||||||
|
#define BMD_DECKLINKAPI_v11_5_1_H
|
||||||
|
|
||||||
|
#include "DeckLinkAPI.h"
|
||||||
|
|
||||||
|
/* Enum BMDDeckLinkStatusID - DeckLink Status ID */
|
||||||
|
|
||||||
|
typedef uint32_t BMDDeckLinkStatusID_v11_5_1;
|
||||||
|
enum _BMDDeckLinkStatusID_v11_5_1 {
|
||||||
|
|
||||||
|
/* Video output flags */
|
||||||
|
|
||||||
|
bmdDeckLinkStatusDetectedVideoInputFlags_v11_5_1 = /* 'dvif' */ 0x64766966,
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /* defined(BMD_DECKLINKAPI_v11_5_1_H) */
|
||||||
110
services/capture/sdk/DeckLinkAPI_v14_2_1.h
Normal file
110
services/capture/sdk/DeckLinkAPI_v14_2_1.h
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
/* -LICENSE-START-
|
||||||
|
** Copyright (c) 2018 Blackmagic Design
|
||||||
|
**
|
||||||
|
** Permission is hereby granted, free of charge, to any person or organization
|
||||||
|
** obtaining a copy of the software and accompanying documentation (the
|
||||||
|
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||||
|
** and transmit the Software, and to prepare derivative works of the Software,
|
||||||
|
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||||
|
** accordance with:
|
||||||
|
**
|
||||||
|
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||||
|
** Agreement for the Software Development Kit (“EULA”) available at
|
||||||
|
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||||
|
**
|
||||||
|
** (2) if the Software is obtained from any third party, such licensing terms
|
||||||
|
** as notified by that third party,
|
||||||
|
**
|
||||||
|
** and all subject to the following:
|
||||||
|
**
|
||||||
|
** (3) the copyright notices in the Software and this entire statement,
|
||||||
|
** including the above license grant, this restriction and the following
|
||||||
|
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||||
|
** part, and all derivative works of the Software, unless such copies or
|
||||||
|
** derivative works are solely in the form of machine-executable object code
|
||||||
|
** generated by a source language processor.
|
||||||
|
**
|
||||||
|
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||||
|
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||||
|
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||||
|
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
** DEALINGS IN THE SOFTWARE.
|
||||||
|
**
|
||||||
|
** A copy of the Software is available free of charge at
|
||||||
|
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||||
|
**
|
||||||
|
** -LICENSE-END-
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef BMD_DECKLINKAPI_v14_2_1_H
|
||||||
|
#define BMD_DECKLINKAPI_v14_2_1_H
|
||||||
|
|
||||||
|
#include "DeckLinkAPI.h"
|
||||||
|
#include "DeckLinkAPIVideoConversion_v14_2_1.h"
|
||||||
|
#include "DeckLinkAPIGLScreenPreview_v14_2_1.h"
|
||||||
|
#include "DeckLinkAPIMetalScreenPreview_v14_2_1.h"
|
||||||
|
#include "DeckLinkAPIScreenPreviewCallback_v14_2_1.h"
|
||||||
|
#include "DeckLinkAPIVideoOutput_v14_2_1.h"
|
||||||
|
#include "DeckLinkAPIVideoInput_v14_2_1.h"
|
||||||
|
#include "DeckLinkAPIVideoFrame3DExtensions_v14_2_1.h"
|
||||||
|
|
||||||
|
// Interface ID Declarations
|
||||||
|
|
||||||
|
BMD_CONST REFIID IID_IDeckLinkEncoderInput_v14_2_1 = /* F222551D-13DF-4FD8-B587-9D4F19EC12C9 */ { 0xF2,0x22,0x55,0x1D,0x13,0xDF,0x4F,0xD8,0xB5,0x87,0x9D,0x4F,0x19,0xEC,0x12,0xC9 };
|
||||||
|
|
||||||
|
#if defined(__cplusplus)
|
||||||
|
|
||||||
|
/* Interface IDeckLinkEncoderInput - Created by QueryInterface from IDeckLink. */
|
||||||
|
|
||||||
|
class BMD_PUBLIC IDeckLinkEncoderInput_v14_2_1 : public IUnknown
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual HRESULT DoesSupportVideoMode (/* in */ BMDVideoConnection connection /* If a value of bmdVideoConnectionUnspecified is specified, the caller does not care about the connection */, /* in */ BMDDisplayMode requestedMode, /* in */ BMDPixelFormat requestedCodec, /* in */ uint32_t requestedCodecProfile, /* in */ BMDSupportedVideoModeFlags flags, /* out */ bool* supported) = 0;
|
||||||
|
virtual HRESULT GetDisplayMode (/* in */ BMDDisplayMode displayMode, /* out */ IDeckLinkDisplayMode** resultDisplayMode) = 0;
|
||||||
|
virtual HRESULT GetDisplayModeIterator (/* out */ IDeckLinkDisplayModeIterator** iterator) = 0;
|
||||||
|
|
||||||
|
/* Video Input */
|
||||||
|
|
||||||
|
virtual HRESULT EnableVideoInput (/* in */ BMDDisplayMode displayMode, /* in */ BMDPixelFormat pixelFormat, /* in */ BMDVideoInputFlags flags) = 0;
|
||||||
|
virtual HRESULT DisableVideoInput (void) = 0;
|
||||||
|
virtual HRESULT GetAvailablePacketsCount (/* out */ uint32_t* availablePacketsCount) = 0;
|
||||||
|
virtual HRESULT SetMemoryAllocator (/* in */ IDeckLinkMemoryAllocator_v14_2_1* theAllocator) = 0;
|
||||||
|
|
||||||
|
/* Audio Input */
|
||||||
|
|
||||||
|
virtual HRESULT EnableAudioInput (/* in */ BMDAudioFormat audioFormat, /* in */ BMDAudioSampleRate sampleRate, /* in */ BMDAudioSampleType sampleType, /* in */ uint32_t channelCount) = 0;
|
||||||
|
virtual HRESULT DisableAudioInput (void) = 0;
|
||||||
|
virtual HRESULT GetAvailableAudioSampleFrameCount (/* out */ uint32_t* availableSampleFrameCount) = 0;
|
||||||
|
|
||||||
|
/* Input Control */
|
||||||
|
|
||||||
|
virtual HRESULT StartStreams (void) = 0;
|
||||||
|
virtual HRESULT StopStreams (void) = 0;
|
||||||
|
virtual HRESULT PauseStreams (void) = 0;
|
||||||
|
virtual HRESULT FlushStreams (void) = 0;
|
||||||
|
virtual HRESULT SetCallback (/* in */ IDeckLinkEncoderInputCallback* theCallback) = 0;
|
||||||
|
|
||||||
|
/* Hardware Timing */
|
||||||
|
|
||||||
|
virtual HRESULT GetHardwareReferenceClock (/* in */ BMDTimeScale desiredTimeScale, /* out */ BMDTimeValue* hardwareTime, /* out */ BMDTimeValue* timeInFrame, /* out */ BMDTimeValue* ticksPerFrame) = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual ~IDeckLinkEncoderInput_v14_2_1 () {} // call Release method to drop reference count
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Functions */
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
BMD_PUBLIC IDeckLinkIterator* CreateDeckLinkIteratorInstance_v14_2_1(void);
|
||||||
|
BMD_PUBLIC IDeckLinkDiscovery* CreateDeckLinkDiscoveryInstance_v14_2_1(void);
|
||||||
|
BMD_PUBLIC IDeckLinkAPIInformation* CreateDeckLinkAPIInformationInstance_v14_2_1(void);
|
||||||
|
BMD_PUBLIC IDeckLinkGLScreenPreviewHelper_v14_2_1* CreateOpenGLScreenPreviewHelper_v14_2_1(void);
|
||||||
|
BMD_PUBLIC IDeckLinkGLScreenPreviewHelper_v14_2_1* CreateOpenGL3ScreenPreviewHelper_v14_2_1(void); // Requires OpenGL 3.2 support and provides improved performance and color handling
|
||||||
|
BMD_PUBLIC IDeckLinkVideoConversion_v14_2_1* CreateVideoConversionInstance_v14_2_1(void);
|
||||||
|
BMD_PUBLIC IDeckLinkVideoFrameAncillaryPackets* CreateVideoFrameAncillaryPacketsInstance_v14_2_1(void); // For use when creating a custom IDeckLinkVideoFrame without wrapping IDeckLinkOutput::CreateVideoFrame
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // defined(__cplusplus)
|
||||||
|
#endif /* defined(BMD_DECKLINKAPI_v14_2_1_H) */
|
||||||
96
services/capture/sdk/DeckLinkAPI_v15_2.h
Normal file
96
services/capture/sdk/DeckLinkAPI_v15_2.h
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
/* -LICENSE-START-
|
||||||
|
** Copyright (c) 2025 Blackmagic Design
|
||||||
|
**
|
||||||
|
** Permission is hereby granted, free of charge, to any person or organization
|
||||||
|
** obtaining a copy of the software and accompanying documentation (the
|
||||||
|
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||||
|
** and transmit the Software, and to prepare derivative works of the Software,
|
||||||
|
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||||
|
** accordance with:
|
||||||
|
**
|
||||||
|
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||||
|
** Agreement for the Software Development Kit ("EULA") available at
|
||||||
|
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||||
|
**
|
||||||
|
** (2) if the Software is obtained from any third party, such licensing terms
|
||||||
|
** as notified by that third party,
|
||||||
|
**
|
||||||
|
** and all subject to the following:
|
||||||
|
**
|
||||||
|
** (3) the copyright notices in the Software and this entire statement,
|
||||||
|
** including the above license grant, this restriction and the following
|
||||||
|
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||||
|
** part, and all derivative works of the Software, unless such copies or
|
||||||
|
** derivative works are solely in the form of machine-executable object code
|
||||||
|
** generated by a source language processor.
|
||||||
|
**
|
||||||
|
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||||
|
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||||
|
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||||
|
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
** DEALINGS IN THE SOFTWARE.
|
||||||
|
**
|
||||||
|
** A copy of the Software is available free of charge at
|
||||||
|
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||||
|
**
|
||||||
|
** -LICENSE-END-
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef BMD_DECKLINKAPI_v15_2_H
|
||||||
|
#define BMD_DECKLINKAPI_v15_2_H
|
||||||
|
|
||||||
|
#include "DeckLinkAPI.h"
|
||||||
|
|
||||||
|
// Interface ID Declarations
|
||||||
|
|
||||||
|
BMD_CONST REFIID IID_IDeckLinkAncillaryPacket_v15_2 = /* CC5BBF7E-029C-4D3B-9158-6000EF5E3670 */ { 0xCC,0x5B,0xBF,0x7E,0x02,0x9C,0x4D,0x3B,0x91,0x58,0x60,0x00,0xEF,0x5E,0x36,0x70 };
|
||||||
|
BMD_CONST REFIID IID_IDeckLinkAncillaryPacketIterator_v15_2 = /* 3FC8994B-88FB-4C17-968F-9AAB69D964A7 */ { 0x3F,0xC8,0x99,0x4B,0x88,0xFB,0x4C,0x17,0x96,0x8F,0x9A,0xAB,0x69,0xD9,0x64,0xA7 };
|
||||||
|
BMD_CONST REFIID IID_IDeckLinkVideoFrameAncillaryPackets_v15_2 = /* 6C186C0F-459E-41D8-AEE2-4812D81AEE68 */ { 0x6C,0x18,0x6C,0x0F,0x45,0x9E,0x41,0xD8,0xAE,0xE2,0x48,0x12,0xD8,0x1A,0xEE,0x68 };
|
||||||
|
|
||||||
|
/* Interface IDeckLinkAncillaryPacket - On output, user needs to implement this interface */
|
||||||
|
|
||||||
|
#if defined(__cplusplus)
|
||||||
|
|
||||||
|
class BMD_PUBLIC IDeckLinkAncillaryPacket_v15_2 : public IUnknown
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual HRESULT GetBytes (/* in */ BMDAncillaryPacketFormat format /* For output, only one format need be offered */, /* out */ const void** data /* Optional */, /* out */ uint32_t* size /* Optional */) = 0;
|
||||||
|
virtual uint8_t GetDID (void) = 0;
|
||||||
|
virtual uint8_t GetSDID (void) = 0;
|
||||||
|
virtual uint32_t GetLineNumber (void) = 0; // On output, zero is auto
|
||||||
|
virtual uint8_t GetDataStreamIndex (void) = 0; // Usually zero. Can only be 1 if non-SD and the first data stream is completely full
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual ~IDeckLinkAncillaryPacket_v15_2 () {} // call Release method to drop reference count
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Interface IDeckLinkAncillaryPacketIterator - Enumerates ancillary packets */
|
||||||
|
|
||||||
|
class BMD_PUBLIC IDeckLinkAncillaryPacketIterator_v15_2 : public IUnknown
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual HRESULT Next (/* out */ IDeckLinkAncillaryPacket_v15_2** packet) = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual ~IDeckLinkAncillaryPacketIterator_v15_2 () {} // call Release method to drop reference count
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Interface IDeckLinkVideoFrameAncillaryPackets - Obtained through QueryInterface on an IDeckLinkVideoFrame object. */
|
||||||
|
|
||||||
|
class BMD_PUBLIC IDeckLinkVideoFrameAncillaryPackets_v15_2 : public IUnknown
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual HRESULT GetPacketIterator (/* out */ IDeckLinkAncillaryPacketIterator_v15_2** iterator) = 0;
|
||||||
|
virtual HRESULT GetFirstPacketByID (/* in */ uint8_t DID, /* in */ uint8_t SDID, /* out */ IDeckLinkAncillaryPacket_v15_2** packet) = 0;
|
||||||
|
virtual HRESULT AttachPacket (/* in */ IDeckLinkAncillaryPacket_v15_2* packet) = 0; // Implement IDeckLinkAncillaryPacket to output your own
|
||||||
|
virtual HRESULT DetachPacket (/* in */ IDeckLinkAncillaryPacket_v15_2* packet) = 0;
|
||||||
|
virtual HRESULT DetachAllPackets (void) = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual ~IDeckLinkVideoFrameAncillaryPackets_v15_2 () {} // call Release method to drop reference count
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /* defined(__cplusplus) */
|
||||||
|
#endif /* defined(BMD_DECKLINKAPI_H) */
|
||||||
189
services/capture/sdk/DeckLinkAPI_v15_3_1.h
Normal file
189
services/capture/sdk/DeckLinkAPI_v15_3_1.h
Normal file
|
|
@ -0,0 +1,189 @@
|
||||||
|
/* -LICENSE-START-
|
||||||
|
** Copyright (c) 2025 Blackmagic Design
|
||||||
|
**
|
||||||
|
** Permission is hereby granted, free of charge, to any person or organization
|
||||||
|
** obtaining a copy of the software and accompanying documentation (the
|
||||||
|
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||||
|
** and transmit the Software, and to prepare derivative works of the Software,
|
||||||
|
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||||
|
** accordance with:
|
||||||
|
**
|
||||||
|
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||||
|
** Agreement for the Software Development Kit (“EULA”) available at
|
||||||
|
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||||
|
**
|
||||||
|
** (2) if the Software is obtained from any third party, such licensing terms
|
||||||
|
** as notified by that third party,
|
||||||
|
**
|
||||||
|
** and all subject to the following:
|
||||||
|
**
|
||||||
|
** (3) the copyright notices in the Software and this entire statement,
|
||||||
|
** including the above license grant, this restriction and the following
|
||||||
|
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||||
|
** part, and all derivative works of the Software, unless such copies or
|
||||||
|
** derivative works are solely in the form of machine-executable object code
|
||||||
|
** generated by a source language processor.
|
||||||
|
**
|
||||||
|
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||||
|
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||||
|
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||||
|
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
** DEALINGS IN THE SOFTWARE.
|
||||||
|
**
|
||||||
|
** A copy of the Software is available free of charge at
|
||||||
|
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||||
|
**
|
||||||
|
** -LICENSE-END-
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "DeckLinkAPI.h"
|
||||||
|
|
||||||
|
// Interface ID Declarations
|
||||||
|
|
||||||
|
BMD_CONST REFIID IID_IDeckLinkStatus_v15_3_1 = /* 5F558200-4028-49BC-BEAC-DB3FA4A96E46 */ { 0x5F,0x55,0x82,0x00,0x40,0x28,0x49,0xBC,0xBE,0xAC,0xDB,0x3F,0xA4,0xA9,0x6E,0x46 };
|
||||||
|
BMD_CONST REFIID IID_IDeckLinkVideoBuffer_v15_3_1 = /* CCB4B64A-5C86-4E02-B778-885D352709FE */ { 0xCC,0xB4,0xB6,0x4A,0x5C,0x86,0x4E,0x02,0xB7,0x78,0x88,0x5D,0x35,0x27,0x09,0xFE };
|
||||||
|
BMD_CONST REFIID IID_IDeckLinkVideoBufferAllocator_v15_3_1 = /* 3481A4DF-2B11-4E55-AC61-836B87985E9A */ { 0x34,0x81,0xA4,0xDF,0x2B,0x11,0x4E,0x55,0xAC,0x61,0x83,0x6B,0x87,0x98,0x5E,0x9A };
|
||||||
|
BMD_CONST REFIID IID_IDeckLinkVideoBufferAllocatorProvider_v15_3_1 = /* 08B80403-BFF2-49D0-B448-8C908B9E9FC9 */ { 0x08,0xB8,0x04,0x03,0xBF,0xF2,0x49,0xD0,0xB4,0x48,0x8C,0x90,0x8B,0x9E,0x9F,0xC9 };
|
||||||
|
BMD_CONST REFIID IID_IDeckLinkVideoConversion_v15_3_1 = /* A48755D9-8BD5-4727-A1E9-069FDEDBA6E9 */ { 0xA4,0x87,0x55,0xD9,0x8B,0xD5,0x47,0x27,0xA1,0xE9,0x06,0x9F,0xDE,0xDB,0xA6,0xE9 };
|
||||||
|
BMD_CONST REFIID IID_IDeckLinkProfileAttributes_v15_3_1 = /* 17D4BF8E-4911-473A-80A0-731CF6FF345B */ { 0x17,0xD4,0xBF,0x8E,0x49,0x11,0x47,0x3A,0x80,0xA0,0x73,0x1C,0xF6,0xFF,0x34,0x5B };
|
||||||
|
BMD_CONST REFIID IID_IDeckLinkNotification_v15_3_1 = /* B85DF4C8-BDF5-47C1-8064-28162EBDD4EB */ { 0xB8,0x5D,0xF4,0xC8,0xBD,0xF5,0x47,0xC1,0x80,0x64,0x28,0x16,0x2E,0xBD,0xD4,0xEB };
|
||||||
|
|
||||||
|
#if defined(__cplusplus)
|
||||||
|
|
||||||
|
/* Enum BMDDeckLinkStatusID_v15_3_1 - DeckLink Status ID */
|
||||||
|
|
||||||
|
typedef uint32_t BMDDeckLinkStatusID_v15_3_1;
|
||||||
|
enum _BMDDeckLinkStatusID_v15_3_1
|
||||||
|
{
|
||||||
|
/* Integers */
|
||||||
|
bmdDeckLinkStatusDeviceTemperature_v15_3_1 = /* 'dtmp' */ 0x64746D70,
|
||||||
|
|
||||||
|
bmdDeckLinkStatusEthernetLink_v15_3_1 = /* 'sels' */ 0x73656C73,
|
||||||
|
bmdDeckLinkStatusEthernetLinkMbps_v15_3_1 = /* 'sesp' */ 0x73657370,
|
||||||
|
|
||||||
|
/* Strings */
|
||||||
|
bmdDeckLinkStatusEthernetLocalIPAddress_v15_3_1 = /* 'seip' */ 0x73656970,
|
||||||
|
bmdDeckLinkStatusEthernetSubnetMask_v15_3_1 = /* 'sesm' */ 0x7365736D,
|
||||||
|
bmdDeckLinkStatusEthernetGatewayIPAddress_v15_3_1 = /* 'segw' */ 0x73656777,
|
||||||
|
bmdDeckLinkStatusEthernetPrimaryDNS_v15_3_1 = /* 'sepd' */ 0x73657064,
|
||||||
|
bmdDeckLinkStatusEthernetSecondaryDNS_v15_3_1 = /* 'sesd' */ 0x73657364,
|
||||||
|
bmdDeckLinkStatusEthernetVideoOutputAddress_v15_3_1 = /* 'soav' */ 0x736F6176,
|
||||||
|
bmdDeckLinkStatusEthernetAudioOutputAddress_v15_3_1 = /* 'soaa' */ 0x736F6161,
|
||||||
|
bmdDeckLinkStatusEthernetAncillaryOutputAddress_v15_3_1 = /* 'soaA' */ 0x736F6141,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Enum BMDDeckLinkAttributeID - DeckLink Attribute ID */
|
||||||
|
|
||||||
|
typedef uint32_t BMDDeckLinkAttributeID_v15_3_1;
|
||||||
|
enum _BMDDeckLinkAttributeID_v15_3_1
|
||||||
|
{
|
||||||
|
/* Strings */
|
||||||
|
BMDDeckLinkEthernetMACAddress_v15_3_1 = /* 'eMAC' */ 0x654D4143,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Interface IDeckLinkStatus_v15_3_1 - DeckLink Status interface */
|
||||||
|
|
||||||
|
class BMD_PUBLIC IDeckLinkStatus_v15_3_1 : public IUnknown
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual HRESULT GetFlag (/* in */ BMDDeckLinkStatusID statusID, /* out */ bool* value) = 0;
|
||||||
|
virtual HRESULT GetInt (/* in */ BMDDeckLinkStatusID statusID, /* out */ int64_t* value) = 0;
|
||||||
|
virtual HRESULT GetFloat (/* in */ BMDDeckLinkStatusID statusID, /* out */ double* value) = 0;
|
||||||
|
virtual HRESULT GetString (/* in */ BMDDeckLinkStatusID statusID, /* out */ const char** value) = 0;
|
||||||
|
virtual HRESULT GetBytes (/* in */ BMDDeckLinkStatusID statusID, /* out */ void* buffer, /* in, out */ uint32_t* bufferSize) = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual ~IDeckLinkStatus_v15_3_1 () {} // call Release method to drop reference count
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Interface IDeckLinkVideoBuffer_v15_3_1 - Interface to encapsulate a video frame buffer; can be caller-implemented. */
|
||||||
|
|
||||||
|
class BMD_PUBLIC IDeckLinkVideoBuffer_v15_3_1 : public IUnknown
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual HRESULT GetBytes (/* out */ void** buffer) = 0;
|
||||||
|
virtual HRESULT StartAccess (/* in */ BMDBufferAccessFlags flags) = 0;
|
||||||
|
virtual HRESULT EndAccess (/* in */ BMDBufferAccessFlags flags) = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual ~IDeckLinkVideoBuffer_v15_3_1 () {} // call Release method to drop reference count
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Interface IDeckLinkVideoBufferAllocator_v15_3_1 - Buffer allocator for video. */
|
||||||
|
|
||||||
|
class BMD_PUBLIC IDeckLinkVideoBufferAllocator_v15_3_1 : public IUnknown
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual HRESULT AllocateVideoBuffer (/* out */ IDeckLinkVideoBuffer_v15_3_1** allocatedBuffer) = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual ~IDeckLinkVideoBufferAllocator_v15_3_1 () {} // call Release method to drop reference count
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Interface IDeckLinkVideoBufferAllocatorProvider_v15_3_1 - Allows EnableVideoInputWithAllocatorProvider to obtain allocators */
|
||||||
|
|
||||||
|
class BMD_PUBLIC IDeckLinkVideoBufferAllocatorProvider_v15_3_1 : public IUnknown
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual HRESULT GetVideoBufferAllocator (/* in */ uint32_t bufferSize, /* in */ uint32_t width, /* in */ uint32_t height, /* in */ uint32_t rowBytes, /* in */ BMDPixelFormat pixelFormat, /* out */ IDeckLinkVideoBufferAllocator_v15_3_1** allocator) = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual ~IDeckLinkVideoBufferAllocatorProvider_v15_3_1 () {} // call Release method to drop reference count
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Interface IDeckLinkVideoConversion_v15_3_1 */
|
||||||
|
|
||||||
|
class BMD_PUBLIC IDeckLinkVideoConversion_v15_3_1 : public IUnknown
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual HRESULT ConvertFrame (/* in */ IDeckLinkVideoFrame* srcFrame, /* in */ IDeckLinkVideoFrame* dstFrame) = 0;
|
||||||
|
virtual HRESULT ConvertNewFrame (/* in */ IDeckLinkVideoFrame* srcFrame, /* in */ BMDPixelFormat dstPixelFormat, /* in */ BMDColorspace dstColorspace, /* in */ IDeckLinkVideoBuffer_v15_3_1* dstBuffer, /* out */ IDeckLinkVideoFrame** dstFrame) = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual ~IDeckLinkVideoConversion_v15_3_1 () {} // call Release method to drop reference count
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Interface IDeckLinkProfileAttributes_v15_3_1 - Created by QueryInterface from an IDeckLinkProfile, or from IDeckLink. When queried from IDeckLink, interrogates the active profile */
|
||||||
|
|
||||||
|
class BMD_PUBLIC IDeckLinkProfileAttributes_v15_3_1 : public IUnknown
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual HRESULT GetFlag (/* in */ BMDDeckLinkAttributeID cfgID, /* out */ bool* value) = 0;
|
||||||
|
virtual HRESULT GetInt (/* in */ BMDDeckLinkAttributeID cfgID, /* out */ int64_t* value) = 0;
|
||||||
|
virtual HRESULT GetFloat (/* in */ BMDDeckLinkAttributeID cfgID, /* out */ double* value) = 0;
|
||||||
|
virtual HRESULT GetString (/* in */ BMDDeckLinkAttributeID cfgID, /* out */ const char** value) = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual ~IDeckLinkProfileAttributes_v15_3_1 () {} // call Release method to drop reference count
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Interface IDeckLinkNotification_v15_3_1 - DeckLink Notification interface */
|
||||||
|
|
||||||
|
class BMD_PUBLIC IDeckLinkNotification_v15_3_1 : public IUnknown
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual HRESULT Subscribe (/* in */ BMDNotifications topic, /* in */ IDeckLinkNotificationCallback *theCallback) = 0;
|
||||||
|
virtual HRESULT Unsubscribe (/* in */ BMDNotifications topic, /* in */ IDeckLinkNotificationCallback *theCallback) = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual ~IDeckLinkNotification_v15_3_1 () {} // call Release method to drop reference count
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Functions */
|
||||||
|
|
||||||
|
extern "C"
|
||||||
|
{
|
||||||
|
BMD_PUBLIC IDeckLinkIterator* CreateDeckLinkIteratorInstance_v15_3_1(void);
|
||||||
|
BMD_PUBLIC IDeckLinkDiscovery* CreateDeckLinkDiscoveryInstance_v15_3_1(void);
|
||||||
|
BMD_PUBLIC IDeckLinkAPIInformation* CreateDeckLinkAPIInformationInstance_v15_3_1(void);
|
||||||
|
BMD_PUBLIC IDeckLinkGLScreenPreviewHelper* CreateOpenGLScreenPreviewHelper_v15_3_1(void);
|
||||||
|
BMD_PUBLIC IDeckLinkGLScreenPreviewHelper* CreateOpenGL3ScreenPreviewHelper_v15_3_1(void); // Requires OpenGL 3.2 support and provides improved performance and color handling
|
||||||
|
BMD_PUBLIC IDeckLinkVideoConversion_v15_3_1* CreateVideoConversionInstance_v15_3_1(void);
|
||||||
|
BMD_PUBLIC IDeckLinkVideoFrameAncillaryPackets* CreateVideoFrameAncillaryPacketsInstance_v15_3_1(void); // For use when creating a custom IDeckLinkVideoFrame without wrapping IDeckLinkOutput::CreateVideoFrame
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // defined(__cplusplus)
|
||||||
116
services/capture/sdk/LinuxCOM.h
Normal file
116
services/capture/sdk/LinuxCOM.h
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
/* -LICENSE-START-
|
||||||
|
** Copyright (c) 2009 Blackmagic Design
|
||||||
|
**
|
||||||
|
** Permission is hereby granted, free of charge, to any person or organization
|
||||||
|
** obtaining a copy of the software and accompanying documentation (the
|
||||||
|
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||||
|
** and transmit the Software, and to prepare derivative works of the Software,
|
||||||
|
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||||
|
** accordance with:
|
||||||
|
**
|
||||||
|
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||||
|
** Agreement for the Software Development Kit ("EULA") available at
|
||||||
|
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||||
|
**
|
||||||
|
** (2) if the Software is obtained from any third party, such licensing terms
|
||||||
|
** as notified by that third party,
|
||||||
|
**
|
||||||
|
** and all subject to the following:
|
||||||
|
**
|
||||||
|
** (3) the copyright notices in the Software and this entire statement,
|
||||||
|
** including the above license grant, this restriction and the following
|
||||||
|
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||||
|
** part, and all derivative works of the Software, unless such copies or
|
||||||
|
** derivative works are solely in the form of machine-executable object code
|
||||||
|
** generated by a source language processor.
|
||||||
|
**
|
||||||
|
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||||
|
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||||
|
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||||
|
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
** DEALINGS IN THE SOFTWARE.
|
||||||
|
**
|
||||||
|
** A copy of the Software is available free of charge at
|
||||||
|
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||||
|
**
|
||||||
|
** -LICENSE-END-
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef __LINUX_COM_H_
|
||||||
|
#define __LINUX_COM_H_
|
||||||
|
|
||||||
|
struct REFIID
|
||||||
|
{
|
||||||
|
unsigned char byte0;
|
||||||
|
unsigned char byte1;
|
||||||
|
unsigned char byte2;
|
||||||
|
unsigned char byte3;
|
||||||
|
unsigned char byte4;
|
||||||
|
unsigned char byte5;
|
||||||
|
unsigned char byte6;
|
||||||
|
unsigned char byte7;
|
||||||
|
unsigned char byte8;
|
||||||
|
unsigned char byte9;
|
||||||
|
unsigned char byte10;
|
||||||
|
unsigned char byte11;
|
||||||
|
unsigned char byte12;
|
||||||
|
unsigned char byte13;
|
||||||
|
unsigned char byte14;
|
||||||
|
unsigned char byte15;
|
||||||
|
};
|
||||||
|
|
||||||
|
typedef REFIID CFUUIDBytes;
|
||||||
|
#define CFUUIDGetUUIDBytes(x) x
|
||||||
|
|
||||||
|
typedef int HRESULT;
|
||||||
|
typedef unsigned long ULONG;
|
||||||
|
typedef void *LPVOID;
|
||||||
|
|
||||||
|
#define SUCCEEDED(Status) ((HRESULT)(Status) >= 0)
|
||||||
|
#define FAILED(Status) ((HRESULT)(Status)<0)
|
||||||
|
|
||||||
|
#define IS_ERROR(Status) ((unsigned long)(Status) >> 31 == SEVERITY_ERROR)
|
||||||
|
#define HRESULT_CODE(hr) ((hr) & 0xFFFF)
|
||||||
|
#define HRESULT_FACILITY(hr) (((hr) >> 16) & 0x1fff)
|
||||||
|
#define HRESULT_SEVERITY(hr) (((hr) >> 31) & 0x1)
|
||||||
|
#define SEVERITY_SUCCESS 0
|
||||||
|
#define SEVERITY_ERROR 1
|
||||||
|
|
||||||
|
#define MAKE_HRESULT(sev,fac,code) ((HRESULT) (((unsigned long)(sev)<<31) | ((unsigned long)(fac)<<16) | ((unsigned long)(code))) )
|
||||||
|
|
||||||
|
#define S_OK ((HRESULT)0x00000000L)
|
||||||
|
#define S_FALSE ((HRESULT)0x00000001L)
|
||||||
|
#define E_UNEXPECTED ((HRESULT)0x8000FFFFL)
|
||||||
|
#define E_NOTIMPL ((HRESULT)0x80000001L)
|
||||||
|
#define E_OUTOFMEMORY ((HRESULT)0x80000002L)
|
||||||
|
#define E_INVALIDARG ((HRESULT)0x80000003L)
|
||||||
|
#define E_NOINTERFACE ((HRESULT)0x80000004L)
|
||||||
|
#define E_POINTER ((HRESULT)0x80000005L)
|
||||||
|
#define E_HANDLE ((HRESULT)0x80000006L)
|
||||||
|
#define E_ABORT ((HRESULT)0x80000007L)
|
||||||
|
#define E_FAIL ((HRESULT)0x80000008L)
|
||||||
|
#define E_ACCESSDENIED ((HRESULT)0x80000009L)
|
||||||
|
|
||||||
|
#define STDMETHODCALLTYPE
|
||||||
|
|
||||||
|
#define IID_IUnknown (REFIID){0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xC0,0x00,0x00,0x00,0x00,0x00,0x00,0x46}
|
||||||
|
#define IUnknownUUID IID_IUnknown
|
||||||
|
|
||||||
|
#ifndef BMD_PUBLIC
|
||||||
|
#define BMD_PUBLIC
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
class BMD_PUBLIC IUnknown
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, LPVOID *ppv) = 0;
|
||||||
|
virtual ULONG STDMETHODCALLTYPE AddRef(void) = 0;
|
||||||
|
virtual ULONG STDMETHODCALLTYPE Release(void) = 0;
|
||||||
|
};
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -63,6 +63,13 @@ async function bootstrapAutoStart() {
|
||||||
const streamKey = envOpt('STREAM_KEY');
|
const streamKey = envOpt('STREAM_KEY');
|
||||||
const sourceUrl = envOpt('SOURCE_URL');
|
const sourceUrl = envOpt('SOURCE_URL');
|
||||||
const device = envInt('DEVICE_INDEX');
|
const device = envInt('DEVICE_INDEX');
|
||||||
|
// SOURCE_CONFIG is the recorder's source_config JSON (set by recorders.js).
|
||||||
|
// For deltacast it carries the capture channel (`port`) and optional `board`.
|
||||||
|
let sourceConfig = {};
|
||||||
|
try { sourceConfig = JSON.parse(process.env.SOURCE_CONFIG || '{}') || {}; }
|
||||||
|
catch (e) { console.warn('[bootstrap] bad SOURCE_CONFIG JSON:', e.message); }
|
||||||
|
const port = Number.isInteger(sourceConfig.port) ? sourceConfig.port : undefined;
|
||||||
|
const board = Number.isInteger(sourceConfig.board) ? sourceConfig.board : undefined;
|
||||||
|
|
||||||
console.log(`[bootstrap] starting ${sourceType} ingest (listen=${listen} port=${listenPort || 'n/a'})...`);
|
console.log(`[bootstrap] starting ${sourceType} ingest (listen=${listen} port=${listenPort || 'n/a'})...`);
|
||||||
try {
|
try {
|
||||||
|
|
@ -72,6 +79,8 @@ async function bootstrapAutoStart() {
|
||||||
binId: envOpt('BIN_ID') || null,
|
binId: envOpt('BIN_ID') || null,
|
||||||
clipName,
|
clipName,
|
||||||
device,
|
device,
|
||||||
|
port,
|
||||||
|
board,
|
||||||
sourceType,
|
sourceType,
|
||||||
sourceUrl,
|
sourceUrl,
|
||||||
listen,
|
listen,
|
||||||
|
|
@ -135,6 +144,15 @@ async function gracefulShutdown(signal) {
|
||||||
console.error('[shutdown] failed to flag empty asset:', e.message);
|
console.error('[shutdown] failed to flag empty asset:', e.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (completed.growingPath) {
|
||||||
|
// Growing-files recorder: the master lives on the SMB share as a .ts,
|
||||||
|
// NOT in S3 yet. The promotion worker (which watches the same share)
|
||||||
|
// uploads it to S3 and enqueues the proxy from the real, finalized key.
|
||||||
|
// We must NOT call /finalize here: that sets original_s3_key to a key
|
||||||
|
// that doesn't exist yet and enqueues a proxy that instantly fails with
|
||||||
|
// "unable to open the file on disk." Leave the asset 'live' for the
|
||||||
|
// promotion worker to flip to 'ready'.
|
||||||
|
console.log(`[shutdown] growing capture finalized on share (${completed.growingPath}); leaving promotion worker to upload + proxy`);
|
||||||
} else if (liveAssetId) {
|
} else if (liveAssetId) {
|
||||||
// Finalise the pre-created live asset by id (avoids POST / 409 collision).
|
// Finalise the pre-created live asset by id (avoids POST / 409 collision).
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -325,9 +325,13 @@ router.post('/start', async (req, res) => {
|
||||||
error: `${source_type.toUpperCase()} caller mode requires: source_url`,
|
error: `${source_type.toUpperCase()} caller mode requires: source_url`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} else if (source_type === 'deltacast') {
|
||||||
|
if (device === undefined || device === null) {
|
||||||
|
return res.status(400).json({ error: 'deltacast source requires: device (board/port index)' });
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
error: `Unknown source_type: ${source_type}. Must be sdi, srt, or rtmp`,
|
error: `Unknown source_type: ${source_type}. Must be sdi, srt, rtmp, or deltacast`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
52
services/mam-api/src/db/migrations/033-playout-scte.sql
Normal file
52
services/mam-api/src/db/migrations/033-playout-scte.sql
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
-- Migration 033 — SCTE-35 ad-break markers for playout.
|
||||||
|
--
|
||||||
|
-- Adds the missing SCTE-35 splice feature to the playout (MCR) subsystem. An
|
||||||
|
-- operator can either schedule an ad break on a channel's timeline (relative to
|
||||||
|
-- the active playlist position, or at a wall-clock time) or fire one immediately
|
||||||
|
-- ("splice now"). Each break is recorded here and, when fired, also written to
|
||||||
|
-- the append-only as-run log so it shows in the compliance record alongside the
|
||||||
|
-- clips that aired.
|
||||||
|
--
|
||||||
|
-- type:
|
||||||
|
-- splice_insert — a scheduled break (out → return), duration_s seconds long
|
||||||
|
-- immediate — fire-now splice (operator pressed "Trigger ad break now")
|
||||||
|
-- splice_out — open-ended avail out (provider break start)
|
||||||
|
-- splice_in — return-to-network (provider break end)
|
||||||
|
--
|
||||||
|
-- status: pending → fired (when the engine acts on it) → done (when the break
|
||||||
|
-- window has elapsed). cancelled is set if the operator removes a pending break.
|
||||||
|
--
|
||||||
|
-- The engine (services/playout) acts on a break by logging the cue, marking the
|
||||||
|
-- as-run row, and — where the output path supports it — injecting a real
|
||||||
|
-- SCTE-35 cue (see playout-manager.triggerScte for the injection point/TODO).
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS playout_scte_breaks (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
channel_id UUID NOT NULL REFERENCES playout_channels(id) ON DELETE CASCADE,
|
||||||
|
-- Position on the active playlist this break should fire after (0-based item
|
||||||
|
-- index). NULL for immediate/wall-clock breaks.
|
||||||
|
playlist_pos INTEGER,
|
||||||
|
-- Wall-clock fire time for scheduled breaks. NULL for immediate breaks.
|
||||||
|
scheduled_at TIMESTAMPTZ,
|
||||||
|
duration_s INTEGER NOT NULL DEFAULT 30,
|
||||||
|
-- SCTE-35 event id (the splice_event_id carried in the cue). Auto-assigned.
|
||||||
|
event_id INTEGER NOT NULL DEFAULT 1,
|
||||||
|
type TEXT NOT NULL DEFAULT 'splice_insert',
|
||||||
|
status TEXT NOT NULL DEFAULT 'pending',
|
||||||
|
fired_at TIMESTAMPTZ,
|
||||||
|
created_by UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
CHECK (type IN ('splice_insert','immediate','splice_out','splice_in')),
|
||||||
|
CHECK (status IN ('pending','fired','done','cancelled'))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_playout_scte_channel ON playout_scte_breaks (channel_id, created_at DESC);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_playout_scte_status ON playout_scte_breaks (status);
|
||||||
|
|
||||||
|
-- As-run gains a 'scte' result so fired breaks land in the compliance log next to
|
||||||
|
-- the clips. The original migration constrained result to played/skipped/error;
|
||||||
|
-- widen it.
|
||||||
|
ALTER TABLE playout_as_run DROP CONSTRAINT IF EXISTS playout_as_run_result_check;
|
||||||
|
ALTER TABLE playout_as_run ADD CONSTRAINT playout_as_run_result_check
|
||||||
|
CHECK (result IN ('played','skipped','error','scte'));
|
||||||
|
|
@ -1,4 +1,16 @@
|
||||||
import { Pool } from 'pg';
|
import { Pool, types } from 'pg';
|
||||||
|
|
||||||
|
// node-postgres returns BIGINT (int8, OID 20) as a *string* by default, because
|
||||||
|
// a 64-bit integer can exceed JS Number.MAX_SAFE_INTEGER. Our int8 columns
|
||||||
|
// (duration_ms, file_size, …) are always well within 2^53, so a string here is
|
||||||
|
// pure footgun: it breaks any consumer that does arithmetic or comparison on the
|
||||||
|
// value (e.g. `duration_ms + x` silently string-concatenates, sorts go
|
||||||
|
// lexicographic, `!ms`/`Math.round` edge cases). Parse int8 to a real Number so
|
||||||
|
// the API always emits numeric duration_ms/file_size in its JSON.
|
||||||
|
//
|
||||||
|
// 20 = int8/bigint OID. Values above Number.MAX_SAFE_INTEGER would lose
|
||||||
|
// precision, but no column in this schema ever reaches that range.
|
||||||
|
types.setTypeParser(20, (val) => (val === null ? null : parseInt(val, 10)));
|
||||||
|
|
||||||
// Prefer DATABASE_URL (set in docker-compose) over individual DB_* vars
|
// Prefer DATABASE_URL (set in docker-compose) over individual DB_* vars
|
||||||
const pool = process.env.DATABASE_URL
|
const pool = process.env.DATABASE_URL
|
||||||
|
|
|
||||||
|
|
@ -41,18 +41,12 @@ import { startCleanupLoop } from './tasks/cleanupTempSegments.js';
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT || 3000;
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
// ── Middleware ────────────────────────────────────────────────────────────────
|
|
||||||
// Tightened CORS — once cookies carry authority, `origin: true` would let
|
|
||||||
// any site forge requests with the cookie. Drive the allowlist from env.
|
|
||||||
const allowedOrigins = (process.env.ALLOWED_ORIGINS || '')
|
const allowedOrigins = (process.env.ALLOWED_ORIGINS || '')
|
||||||
.split(',').map(s => s.trim()).filter(Boolean);
|
.split(',').map(s => s.trim()).filter(Boolean);
|
||||||
app.use(cors({
|
app.use(cors({
|
||||||
origin: (origin, cb) => {
|
origin: (origin, cb) => {
|
||||||
// No Origin header (same-origin or curl) — allow.
|
|
||||||
if (!origin) return cb(null, true);
|
if (!origin) return cb(null, true);
|
||||||
if (allowedOrigins.length === 0 || allowedOrigins.includes(origin)) return cb(null, true);
|
if (allowedOrigins.length === 0 || allowedOrigins.includes(origin)) return cb(null, true);
|
||||||
// Reject cleanly: omit the Allow-Origin header so the browser surfaces
|
|
||||||
// a real CORS error instead of a 500 from a thrown Error in the callback.
|
|
||||||
console.warn('[cors] rejected origin:', origin);
|
console.warn('[cors] rejected origin:', origin);
|
||||||
return cb(null, false);
|
return cb(null, false);
|
||||||
},
|
},
|
||||||
|
|
@ -60,14 +54,8 @@ app.use(cors({
|
||||||
}));
|
}));
|
||||||
app.use(express.json({ limit: '50mb' }));
|
app.use(express.json({ limit: '50mb' }));
|
||||||
|
|
||||||
// Trust the reverse proxy only when explicitly told to (production HTTPS).
|
|
||||||
if (process.env.TRUST_PROXY === 'true') app.set('trust proxy', 1);
|
if (process.env.TRUST_PROXY === 'true') app.set('trust proxy', 1);
|
||||||
|
|
||||||
// HSTS — once a browser has seen this header over HTTPS for dragonflight.live,
|
|
||||||
// it auto-upgrades every future http:// request to https:// before hitting the
|
|
||||||
// wire. Cookies are Secure-only (below) and the CORS allowlist rejects HTTP,
|
|
||||||
// so without HSTS a user who lands on http:// silently can't log in.
|
|
||||||
// Only emit on actual HTTPS responses; req.secure honors trust proxy + X-Forwarded-Proto.
|
|
||||||
if (process.env.AUTH_ENABLED === 'true') {
|
if (process.env.AUTH_ENABLED === 'true') {
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
if (req.secure) res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
|
if (req.secure) res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
|
||||||
|
|
@ -75,17 +63,13 @@ if (process.env.AUTH_ENABLED === 'true') {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hard-fail when production-mode auth has no stable session secret. Without
|
|
||||||
// this, express-session falls back to an in-memory random secret which
|
|
||||||
// invalidates every session on restart and breaks multi-node deployments.
|
|
||||||
if (process.env.AUTH_ENABLED === 'true' && !process.env.SESSION_SECRET) {
|
if (process.env.AUTH_ENABLED === 'true' && !process.env.SESSION_SECRET) {
|
||||||
console.error('[fatal] SESSION_SECRET is required when AUTH_ENABLED=true');
|
console.error('[fatal] SESSION_SECRET is required when AUTH_ENABLED=true');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Session — actually wired this time. See specs/2026-05-27-auth-system-design.md.
|
|
||||||
app.use(session({
|
app.use(session({
|
||||||
store: new PgStore({ pool, tableName: 'sessions', pruneSessionInterval: 60 * 15 /* seconds = 15 min */ }),
|
store: new PgStore({ pool, tableName: 'sessions', pruneSessionInterval: 60 * 15 }),
|
||||||
secret: process.env.SESSION_SECRET,
|
secret: process.env.SESSION_SECRET,
|
||||||
name: 'dragonflight.sid',
|
name: 'dragonflight.sid',
|
||||||
cookie: {
|
cookie: {
|
||||||
|
|
@ -95,36 +79,26 @@ app.use(session({
|
||||||
path: '/',
|
path: '/',
|
||||||
maxAge: 8 * 3600 * 1000,
|
maxAge: 8 * 3600 * 1000,
|
||||||
},
|
},
|
||||||
rolling: false, // sliding renewal handled in requireAuth so idle + absolute can be enforced separately
|
rolling: false,
|
||||||
resave: false,
|
resave: false,
|
||||||
saveUninitialized: false,
|
saveUninitialized: false,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// ── Health ────────────────────────────────────────────────────────────────────
|
|
||||||
app.get('/health', (_req, res) => res.json({ status: 'ok' }));
|
app.get('/health', (_req, res) => res.json({ status: 'ok' }));
|
||||||
|
|
||||||
// ── Auth gate ─────────────────────────────────────────────────────────────────
|
|
||||||
// req.path is relative to the /api/v1 mount, so /auth/login NOT /api/v1/auth/login.
|
|
||||||
const UNAUTH_PATHS = new Set([
|
const UNAUTH_PATHS = new Set([
|
||||||
'/auth/login', '/auth/login/totp', '/auth/setup', '/auth/setup-required',
|
'/auth/login', '/auth/login/totp', '/auth/setup', '/auth/setup-required',
|
||||||
'/auth/google', '/auth/google/callback', '/auth/google/enabled',
|
'/auth/google', '/auth/google/callback', '/auth/google/enabled',
|
||||||
]);
|
]);
|
||||||
// node-agent now authenticates /cluster/heartbeat with a bound api_token
|
|
||||||
// (migration 019 + bound_hostname on the token). requireAuth handles the
|
|
||||||
// bearer lookup and sets req.tokenBoundHostname; the heartbeat handler in
|
|
||||||
// routes/cluster.js verifies body.hostname matches that binding.
|
|
||||||
app.use('/api/v1', requireUiHeader);
|
app.use('/api/v1', requireUiHeader);
|
||||||
app.use('/api/v1', (req, res, next) => {
|
app.use('/api/v1', (req, res, next) => {
|
||||||
if (UNAUTH_PATHS.has(req.path)) return next();
|
if (UNAUTH_PATHS.has(req.path)) return next();
|
||||||
return requireAuth(req, res, next);
|
return requireAuth(req, res, next);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── API Routes ────────────────────────────────────────────────────────────────
|
|
||||||
app.use('/api/v1/auth', authRouter);
|
app.use('/api/v1/auth', authRouter);
|
||||||
// User and group administration is admin-only (RBAC v2). The auth gate above
|
|
||||||
// already established req.user; requireAdmin rejects non-admins with 403.
|
|
||||||
app.use('/api/v1/auth/users', requireAdmin, usersRouter);
|
app.use('/api/v1/auth/users', requireAdmin, usersRouter);
|
||||||
app.use('/api/v1/users', requireAdmin, usersRouter); // alias for the SPA Users page
|
app.use('/api/v1/users', requireAdmin, usersRouter);
|
||||||
app.use('/api/v1/auth/tokens', requireAuth, tokensRouter);
|
app.use('/api/v1/auth/tokens', requireAuth, tokensRouter);
|
||||||
app.use('/api/v1/assets', assetsRouter);
|
app.use('/api/v1/assets', assetsRouter);
|
||||||
app.use('/api/v1/projects', projectsRouter);
|
app.use('/api/v1/projects', projectsRouter);
|
||||||
|
|
@ -147,21 +121,14 @@ app.use('/api/v1/assets/:assetId/comments', commentsRouter);
|
||||||
app.use('/api/v1/imports', importsRouter);
|
app.use('/api/v1/imports', importsRouter);
|
||||||
app.use('/api/v1/storage', storageRouter);
|
app.use('/api/v1/storage', storageRouter);
|
||||||
|
|
||||||
// ── Error handler ─────────────────────────────────────────────────────────────
|
|
||||||
app.use(errorHandler);
|
app.use(errorHandler);
|
||||||
|
|
||||||
// ── Start ────────────────────────────────────────────────────────────────────
|
|
||||||
import { readdirSync, readFileSync } from 'node:fs';
|
import { readdirSync, readFileSync } from 'node:fs';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { dirname, join } from 'node:path';
|
import { dirname, join } from 'node:path';
|
||||||
|
|
||||||
const __dirnameMig = dirname(fileURLToPath(import.meta.url));
|
const __dirnameMig = dirname(fileURLToPath(import.meta.url));
|
||||||
async function runMigrations() {
|
async function runMigrations() {
|
||||||
// Issue #107 — previously the loop swallowed errors and let the server boot
|
|
||||||
// on a half-migrated schema. Now: track applied migrations in a table, run
|
|
||||||
// every pending one inside a transaction, and exit non-zero on failure so
|
|
||||||
// the orchestrator restarts (and so an operator notices) instead of serving
|
|
||||||
// 500s for the next month.
|
|
||||||
const dir = join(__dirnameMig, 'db', 'migrations');
|
const dir = join(__dirnameMig, 'db', 'migrations');
|
||||||
let files = [];
|
let files = [];
|
||||||
try { files = readdirSync(dir).filter(f => f.endsWith('.sql')).sort(); } catch { return; }
|
try { files = readdirSync(dir).filter(f => f.endsWith('.sql')).sort(); } catch { return; }
|
||||||
|
|
@ -174,7 +141,6 @@ async function runMigrations() {
|
||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// Allow forcing a re-run via env when iterating locally.
|
|
||||||
const force = process.env.MIGRATIONS_FORCE === '1';
|
const force = process.env.MIGRATIONS_FORCE === '1';
|
||||||
const allowFailures = process.env.MIGRATIONS_ALLOW_FAILURES === '1';
|
const allowFailures = process.env.MIGRATIONS_ALLOW_FAILURES === '1';
|
||||||
|
|
||||||
|
|
@ -200,7 +166,6 @@ async function runMigrations() {
|
||||||
console.error('[migration] FAILED ' + f + ': ' + err.message);
|
console.error('[migration] FAILED ' + f + ': ' + err.message);
|
||||||
client.release();
|
client.release();
|
||||||
if (allowFailures) continue;
|
if (allowFailures) continue;
|
||||||
// Hard fail — better to crash now than serve traffic on a broken schema.
|
|
||||||
console.error('[migration] aborting startup. Set MIGRATIONS_ALLOW_FAILURES=1 to override.');
|
console.error('[migration] aborting startup. Set MIGRATIONS_ALLOW_FAILURES=1 to override.');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
@ -209,13 +174,9 @@ async function runMigrations() {
|
||||||
}
|
}
|
||||||
await runMigrations();
|
await runMigrations();
|
||||||
|
|
||||||
// Load S3 config from DB so any settings saved via the Settings page override env vars
|
|
||||||
await loadS3ConfigFromDb();
|
await loadS3ConfigFromDb();
|
||||||
|
|
||||||
// ── Cluster self-heartbeat ────────────────────────────────────────────────────
|
|
||||||
function getLocalIp() {
|
function getLocalIp() {
|
||||||
// Prefer an explicit override — useful when running inside Docker where
|
|
||||||
// os.networkInterfaces() returns container bridge IPs, not the host LAN IP.
|
|
||||||
if (process.env.NODE_IP) return process.env.NODE_IP;
|
if (process.env.NODE_IP) return process.env.NODE_IP;
|
||||||
|
|
||||||
const ifaces = os.networkInterfaces();
|
const ifaces = os.networkInterfaces();
|
||||||
|
|
@ -227,9 +188,6 @@ function getLocalIp() {
|
||||||
return '127.0.0.1';
|
return '127.0.0.1';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect NVIDIA GPUs available to this container via nvidia-smi.
|
|
||||||
// Returns an array like [{ index: 0, name: 'Tesla P4', memory_mb: 7680 }, ...]
|
|
||||||
// or an empty array if nvidia-smi is unavailable or no GPUs found.
|
|
||||||
function detectGpus() {
|
function detectGpus() {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
exec(
|
exec(
|
||||||
|
|
@ -251,6 +209,10 @@ function detectGpus() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Primary mam-api node self-registers in cluster_nodes every 30s. Must write
|
||||||
|
// BOTH last_seen (legacy column) and last_seen_at (added by mig 031, used by
|
||||||
|
// playout failover) — otherwise the primary appears stale to the failover
|
||||||
|
// query and channels get re-placed off it incorrectly.
|
||||||
async function selfHeartbeat() {
|
async function selfHeartbeat() {
|
||||||
const load = os.loadavg()[0];
|
const load = os.loadavg()[0];
|
||||||
const total = os.totalmem();
|
const total = os.totalmem();
|
||||||
|
|
@ -262,14 +224,15 @@ async function selfHeartbeat() {
|
||||||
pool.query(
|
pool.query(
|
||||||
`INSERT INTO cluster_nodes
|
`INSERT INTO cluster_nodes
|
||||||
(hostname, ip_address, role, version, api_url,
|
(hostname, ip_address, role, version, api_url,
|
||||||
cpu_usage, mem_used_mb, mem_total_mb, capabilities, last_seen)
|
cpu_usage, mem_used_mb, mem_total_mb, capabilities, last_seen, last_seen_at)
|
||||||
VALUES ($1,$2,'primary',$3,$4,$5,$6,$7,$8,NOW())
|
VALUES ($1,$2,'primary',$3,$4,$5,$6,$7,$8,NOW(),NOW())
|
||||||
ON CONFLICT (hostname) DO UPDATE SET
|
ON CONFLICT (hostname) DO UPDATE SET
|
||||||
ip_address = EXCLUDED.ip_address,
|
ip_address = EXCLUDED.ip_address,
|
||||||
cpu_usage = EXCLUDED.cpu_usage,
|
cpu_usage = EXCLUDED.cpu_usage,
|
||||||
mem_used_mb = EXCLUDED.mem_used_mb,
|
mem_used_mb = EXCLUDED.mem_used_mb,
|
||||||
mem_total_mb = EXCLUDED.mem_total_mb,
|
mem_total_mb = EXCLUDED.mem_total_mb,
|
||||||
capabilities = EXCLUDED.capabilities,
|
capabilities = EXCLUDED.capabilities,
|
||||||
|
last_seen_at = NOW(),
|
||||||
last_seen = NOW()`,
|
last_seen = NOW()`,
|
||||||
[
|
[
|
||||||
process.env.NODE_HOSTNAME || os.hostname(),
|
process.env.NODE_HOSTNAME || os.hostname(),
|
||||||
|
|
@ -294,39 +257,26 @@ const server = app.listen(PORT, () => {
|
||||||
if (process.env.AUTH_ENABLED === 'true' && process.env.TRUST_PROXY !== 'true') {
|
if (process.env.AUTH_ENABLED === 'true' && process.env.TRUST_PROXY !== 'true') {
|
||||||
console.warn('[auth] WARNING: AUTH_ENABLED=true but TRUST_PROXY=false — req.ip will be the proxy IP, login rate-limit will throttle all clients together. Set TRUST_PROXY=true when behind nginx/HTTPS.');
|
console.warn('[auth] WARNING: AUTH_ENABLED=true but TRUST_PROXY=false — req.ip will be the proxy IP, login rate-limit will throttle all clients together. Set TRUST_PROXY=true when behind nginx/HTTPS.');
|
||||||
}
|
}
|
||||||
// Boot the recorder scheduler tick loop after the HTTP server is live so
|
|
||||||
// the loop's self-calls to /recorders/:id/start|stop reach a ready socket.
|
|
||||||
startSchedulerLoop();
|
startSchedulerLoop();
|
||||||
|
|
||||||
// Boot the temp-segment cleanup loop (runs hourly).
|
|
||||||
startCleanupLoop();
|
startCleanupLoop();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Issue #100 — graceful shutdown. Without this, `docker stop` (SIGTERM) killed
|
|
||||||
// the process mid-scheduler-tick, leaving Redis connections and Docker
|
|
||||||
// sockets dangling and producing partial DB writes. Now: stop the scheduler,
|
|
||||||
// finish in-flight HTTP requests, close PG/Redis pools, and exit cleanly
|
|
||||||
// (or hard-exit after 25 s if something is stuck).
|
|
||||||
let _shuttingDown = false;
|
let _shuttingDown = false;
|
||||||
async function gracefulShutdown(signal) {
|
async function gracefulShutdown(signal) {
|
||||||
if (_shuttingDown) return;
|
if (_shuttingDown) return;
|
||||||
_shuttingDown = true;
|
_shuttingDown = true;
|
||||||
console.log(`[shutdown] received ${signal} — closing gracefully…`);
|
console.log(`[shutdown] received ${signal} — closing gracefully…`);
|
||||||
|
|
||||||
// Stop accepting new requests + wind down the scheduler tick.
|
|
||||||
try { stopSchedulerLoop(); } catch (_) {}
|
try { stopSchedulerLoop(); } catch (_) {}
|
||||||
|
|
||||||
// Force-exit watchdog so a hung connection can't keep us alive forever.
|
|
||||||
const killSwitch = setTimeout(() => {
|
const killSwitch = setTimeout(() => {
|
||||||
console.error('[shutdown] forced exit after 25s timeout');
|
console.error('[shutdown] forced exit after 25s timeout');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}, 25_000);
|
}, 25_000);
|
||||||
killSwitch.unref();
|
killSwitch.unref();
|
||||||
|
|
||||||
// Stop the HTTP server (waits for in-flight requests to finish).
|
|
||||||
await new Promise(resolve => server.close(resolve));
|
await new Promise(resolve => server.close(resolve));
|
||||||
|
|
||||||
// Close DB pool + S3 client + any other resources. Best-effort.
|
|
||||||
try { await pool.end(); } catch (e) { console.warn('[shutdown] pool.end:', e.message); }
|
try { await pool.end(); } catch (e) { console.warn('[shutdown] pool.end:', e.message); }
|
||||||
|
|
||||||
console.log('[shutdown] clean exit');
|
console.log('[shutdown] clean exit');
|
||||||
|
|
|
||||||
|
|
@ -742,11 +742,13 @@ router.get('/:id/live-path', async (req, res, next) => {
|
||||||
const cfg = {};
|
const cfg = {};
|
||||||
for (const { key, value } of s.rows) cfg[key] = value;
|
for (const { key, value } of s.rows) cfg[key] = value;
|
||||||
if (!cfg.growing_smb_url) return res.status(409).json({ error: 'No SMB URL configured — set the editor SMB URL in Settings → Storage' });
|
if (!cfg.growing_smb_url) return res.status(409).json({ error: 'No SMB URL configured — set the editor SMB URL in Settings → Storage' });
|
||||||
const rec = await pool.query(
|
// The growing master is ALWAYS MXF OP1a (XDCAM HD422) on the share, regardless
|
||||||
`SELECT recording_container FROM recorders WHERE current_session_id = $1 ORDER BY updated_at DESC LIMIT 1`,
|
// of the recorder's configured finalized container — that is the format
|
||||||
[asset.id]
|
// Premiere supports for edit-while-record growing files (incremental index
|
||||||
);
|
// segments written into body partitions, readable with no footer). The file
|
||||||
const ext = rec.rows[0]?.recording_container || 'mov';
|
// on the share is `<clip>.mxf`. Keep this in lock-step with GROWING_EXT in
|
||||||
|
// services/capture/src/capture-manager.js.
|
||||||
|
const ext = 'mxf';
|
||||||
const smbRoot = cfg.growing_smb_url.replace(/\/+$/, '');
|
const smbRoot = cfg.growing_smb_url.replace(/\/+$/, '');
|
||||||
const winPath = smbRoot.replace(/^smb:\/\//, '\\\\').replace(/\//g, '\\') + `\\${asset.project_id}\\${asset.display_name}.${ext}`;
|
const winPath = smbRoot.replace(/^smb:\/\//, '\\\\').replace(/\//g, '\\') + `\\${asset.project_id}\\${asset.display_name}.${ext}`;
|
||||||
const posix = smbRoot.replace(/^smb:\/\//, '//') + `/${asset.project_id}/${asset.display_name}.${ext}`;
|
const posix = smbRoot.replace(/^smb:\/\//, '//') + `/${asset.project_id}/${asset.display_name}.${ext}`;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,27 @@
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import http from 'http';
|
import http from 'http';
|
||||||
|
import os from 'os';
|
||||||
import pool from '../db/pool.js';
|
import pool from '../db/pool.js';
|
||||||
import { requireAdmin } from '../middleware/auth.js';
|
import { requireAdmin } from '../middleware/auth.js';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
// Hostname the primary mam-api self-registers as (mirrors selfHeartbeat()).
|
||||||
|
const SELF_HOSTNAME = process.env.NODE_HOSTNAME || os.hostname();
|
||||||
|
|
||||||
|
// Format a process uptime (seconds) the way the Cluster UI expects — a short
|
||||||
|
// human string like "3d 4h" / "12m". Workers don't report uptime today, so the
|
||||||
|
// primary is the only row that populates this.
|
||||||
|
function formatUptime(seconds) {
|
||||||
|
const s = Math.floor(seconds);
|
||||||
|
const d = Math.floor(s / 86400);
|
||||||
|
const h = Math.floor((s % 86400) / 3600);
|
||||||
|
const m = Math.floor((s % 3600) / 60);
|
||||||
|
if (d > 0) return `${d}d ${h}h`;
|
||||||
|
if (h > 0) return `${h}h ${m}m`;
|
||||||
|
return `${m}m`;
|
||||||
|
}
|
||||||
|
|
||||||
// GET /onboard-info – admin-only. Supplies the Add Node wizard with the bits it
|
// GET /onboard-info – admin-only. Supplies the Add Node wizard with the bits it
|
||||||
// needs to build a `curl … | bash` onboarding command: the primary API URL the
|
// needs to build a `curl … | bash` onboarding command: the primary API URL the
|
||||||
// remote node-agent should heartbeat to, the raw URL of onboard-node.sh, and
|
// remote node-agent should heartbeat to, the raw URL of onboard-node.sh, and
|
||||||
|
|
@ -55,7 +72,6 @@ function dockerRequest(path, method = 'GET', body = null) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET / – list all registered cluster nodes with online status
|
|
||||||
router.get('/', async (req, res, next) => {
|
router.get('/', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const r = await pool.query(
|
const r = await pool.query(
|
||||||
|
|
@ -64,25 +80,45 @@ router.get('/', async (req, res, next) => {
|
||||||
FROM cluster_nodes
|
FROM cluster_nodes
|
||||||
ORDER BY registered_at ASC`
|
ORDER BY registered_at ASC`
|
||||||
);
|
);
|
||||||
res.json(r.rows.map(row => ({
|
res.json(r.rows.map(row => {
|
||||||
...row,
|
const out = { ...row, online: Number(row.stale_seconds) < 120 };
|
||||||
online: Number(row.stale_seconds) < 120,
|
// The primary (this mam-api host) does not heartbeat via the node-agent,
|
||||||
})));
|
// so its version/uptime are never populated. Self-populate them here so
|
||||||
|
// the Cluster screen renders them like worker nodes instead of dashes.
|
||||||
|
if (row.role === 'primary' && row.hostname === SELF_HOSTNAME) {
|
||||||
|
out.version = process.env.npm_package_version || row.version || null;
|
||||||
|
out.uptime = formatUptime(process.uptime());
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}));
|
||||||
} catch (err) { next(err); }
|
} catch (err) { next(err); }
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /containers – list all containers on the local Docker host
|
|
||||||
router.get('/containers', async (req, res, next) => {
|
router.get('/containers', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const containers = await dockerRequest('/containers/json?all=true');
|
const containers = await dockerRequest('/containers/json?all=true');
|
||||||
if (!Array.isArray(containers)) return res.json([]);
|
if (!Array.isArray(containers)) return res.json([]);
|
||||||
const out = containers.map(c => {
|
const out = await Promise.all(containers.map(async c => {
|
||||||
const rawName = (c.Names[0] || '').replace(/^\//, '');
|
const rawName = (c.Names[0] || '').replace(/^\//, '');
|
||||||
const name = rawName.replace(/^wild-dragon-/, '').replace(/-\d+$/, '');
|
const name = rawName.replace(/^wild-dragon-/, '').replace(/-\d+$/, '');
|
||||||
const ports = (c.Ports || [])
|
const ports = (c.Ports || [])
|
||||||
.filter(p => p.PublicPort)
|
.filter(p => p.PublicPort)
|
||||||
.map(p => `${p.PublicPort}→${p.PrivatePort}`)
|
.map(p => `${p.PublicPort}→${p.PrivatePort}`)
|
||||||
.join(', ');
|
.join(', ');
|
||||||
|
// Live memory usage requires a per-container stats call (the list endpoint
|
||||||
|
// doesn't include it). One extra Docker call each, but the list is small.
|
||||||
|
// memory_stats.usage includes page cache; subtract it to match `docker stats`.
|
||||||
|
let memBytes = null;
|
||||||
|
if (c.State === 'running') {
|
||||||
|
try {
|
||||||
|
const stats = await dockerRequest(`/containers/${c.Id}/stats?stream=false`);
|
||||||
|
const ms = stats && stats.memory_stats;
|
||||||
|
if (ms && typeof ms.usage === 'number') {
|
||||||
|
const cache = (ms.stats && ms.stats.cache) || 0;
|
||||||
|
memBytes = ms.usage - cache;
|
||||||
|
}
|
||||||
|
} catch (_) { memBytes = null; }
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
id: c.Id.slice(0, 12),
|
id: c.Id.slice(0, 12),
|
||||||
name,
|
name,
|
||||||
|
|
@ -92,9 +128,9 @@ router.get('/containers', async (req, res, next) => {
|
||||||
healthy: (c.Status || '').includes('healthy'),
|
healthy: (c.Status || '').includes('healthy'),
|
||||||
ports,
|
ports,
|
||||||
cpu: 0,
|
cpu: 0,
|
||||||
mem: 0,
|
memBytes,
|
||||||
};
|
};
|
||||||
});
|
}));
|
||||||
res.json(out);
|
res.json(out);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.code === 'ENOENT' || err.code === 'EACCES') return res.json([]);
|
if (err.code === 'ENOENT' || err.code === 'EACCES') return res.json([]);
|
||||||
|
|
@ -102,7 +138,6 @@ router.get('/containers', async (req, res, next) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// POST /containers/:nameOrId/restart
|
|
||||||
router.post('/containers/:nameOrId/restart', async (req, res, next) => {
|
router.post('/containers/:nameOrId/restart', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
await dockerRequest(`/containers/${encodeURIComponent(req.params.nameOrId)}/restart`, 'POST');
|
await dockerRequest(`/containers/${encodeURIComponent(req.params.nameOrId)}/restart`, 'POST');
|
||||||
|
|
@ -110,7 +145,6 @@ router.post('/containers/:nameOrId/restart', async (req, res, next) => {
|
||||||
} catch (err) { next(err); }
|
} catch (err) { next(err); }
|
||||||
});
|
});
|
||||||
|
|
||||||
// POST /heartbeat – upsert this node's registration (includes hardware capabilities)
|
|
||||||
router.post('/heartbeat', async (req, res, next) => {
|
router.post('/heartbeat', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
|
|
@ -122,11 +156,6 @@ router.post('/heartbeat', async (req, res, next) => {
|
||||||
|
|
||||||
if (!hostname) return res.status(400).json({ error: 'hostname is required' });
|
if (!hostname) return res.status(400).json({ error: 'hostname is required' });
|
||||||
|
|
||||||
// Issue #106 — any authenticated user used to be able to POST a heartbeat
|
|
||||||
// for an arbitrary hostname and overwrite the primary node's `api_url`,
|
|
||||||
// effectively hijacking job dispatch. Now: if the caller's token is bound
|
|
||||||
// to a hostname (node-agent tokens are bound at issue time), the body
|
|
||||||
// hostname must match. Admin users with no binding are allowed for ops.
|
|
||||||
if (process.env.AUTH_ENABLED === 'true') {
|
if (process.env.AUTH_ENABLED === 'true') {
|
||||||
const bound = req.tokenBoundHostname;
|
const bound = req.tokenBoundHostname;
|
||||||
if (bound && bound !== hostname) {
|
if (bound && bound !== hostname) {
|
||||||
|
|
@ -146,8 +175,8 @@ router.post('/heartbeat', async (req, res, next) => {
|
||||||
const r = await pool.query(
|
const r = await pool.query(
|
||||||
`INSERT INTO cluster_nodes
|
`INSERT INTO cluster_nodes
|
||||||
(hostname, ip_address, role, version, api_url,
|
(hostname, ip_address, role, version, api_url,
|
||||||
cpu_usage, mem_used_mb, mem_total_mb, last_seen, capabilities, metadata, metrics)
|
cpu_usage, mem_used_mb, mem_total_mb, last_seen, last_seen_at, capabilities, metadata, metrics)
|
||||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,NOW(),$9,$10,$11)
|
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,NOW(),NOW(),$9,$10,$11)
|
||||||
ON CONFLICT (hostname) DO UPDATE SET
|
ON CONFLICT (hostname) DO UPDATE SET
|
||||||
ip_address = EXCLUDED.ip_address,
|
ip_address = EXCLUDED.ip_address,
|
||||||
role = EXCLUDED.role,
|
role = EXCLUDED.role,
|
||||||
|
|
@ -157,6 +186,7 @@ router.post('/heartbeat', async (req, res, next) => {
|
||||||
mem_used_mb = EXCLUDED.mem_used_mb,
|
mem_used_mb = EXCLUDED.mem_used_mb,
|
||||||
mem_total_mb = EXCLUDED.mem_total_mb,
|
mem_total_mb = EXCLUDED.mem_total_mb,
|
||||||
last_seen = NOW(),
|
last_seen = NOW(),
|
||||||
|
last_seen_at = NOW(),
|
||||||
capabilities = EXCLUDED.capabilities,
|
capabilities = EXCLUDED.capabilities,
|
||||||
metadata = EXCLUDED.metadata,
|
metadata = EXCLUDED.metadata,
|
||||||
metrics = COALESCE(EXCLUDED.metrics, cluster_nodes.metrics)
|
metrics = COALESCE(EXCLUDED.metrics, cluster_nodes.metrics)
|
||||||
|
|
@ -179,42 +209,25 @@ router.post('/heartbeat', async (req, res, next) => {
|
||||||
} catch (err) { next(err); }
|
} catch (err) { next(err); }
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /devices/blackmagic/signal – live video-presence state for every
|
|
||||||
// DeckLink port across the cluster. For each port we check whether there is
|
|
||||||
// an active SDI recorder assigned to it and, if so, query the capture
|
|
||||||
// container for its real signal state (receiving / lost / connecting /
|
|
||||||
// error). Ports without a recorder get signal = 'no-recorder'.
|
|
||||||
//
|
|
||||||
// Response shape (array):
|
|
||||||
// { node_id, hostname, index, device, model,
|
|
||||||
// signal, framesReceived, currentFps, recorder_id, recorder_status }
|
|
||||||
router.get('/devices/blackmagic/signal', async (req, res, next) => {
|
router.get('/devices/blackmagic/signal', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
// 1. Fetch all cluster nodes with DeckLink capabilities.
|
|
||||||
const nodesResult = await pool.query(
|
const nodesResult = await pool.query(
|
||||||
`SELECT id, hostname, ip_address, api_url, capabilities,
|
`SELECT id, hostname, ip_address, api_url, capabilities,
|
||||||
EXTRACT(EPOCH FROM (NOW() - last_seen)) AS stale_seconds
|
EXTRACT(EPOCH FROM (NOW() - last_seen)) AS stale_seconds
|
||||||
FROM cluster_nodes
|
FROM cluster_nodes
|
||||||
WHERE capabilities IS NOT NULL`
|
WHERE capabilities IS NOT NULL`
|
||||||
);
|
);
|
||||||
|
|
||||||
// 2. Fetch all SDI recorders that are pinned to a node+device_index.
|
|
||||||
const recResult = await pool.query(
|
const recResult = await pool.query(
|
||||||
`SELECT id, name, status, container_id, node_id, device_index,
|
`SELECT id, name, status, container_id, node_id, device_index,
|
||||||
source_config
|
source_config
|
||||||
FROM recorders
|
FROM recorders
|
||||||
WHERE source_type = 'sdi' AND node_id IS NOT NULL`
|
WHERE source_type = 'sdi' AND node_id IS NOT NULL`
|
||||||
);
|
);
|
||||||
|
|
||||||
// Build a fast lookup: "${node_id}:${device_index}" → recorder row.
|
|
||||||
const recByPort = new Map();
|
const recByPort = new Map();
|
||||||
for (const r of recResult.rows) {
|
for (const r of recResult.rows) {
|
||||||
const devIdx = r.device_index ?? r.source_config?.device ?? 0;
|
const devIdx = r.device_index ?? r.source_config?.device ?? 0;
|
||||||
recByPort.set(`${r.node_id}:${devIdx}`, r);
|
recByPort.set(`${r.node_id}:${devIdx}`, r);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. For each port, determine signal state. We fire all capture-container
|
|
||||||
// fetches concurrently so the endpoint stays fast even with many ports.
|
|
||||||
const tasks = [];
|
const tasks = [];
|
||||||
for (const node of nodesResult.rows) {
|
for (const node of nodesResult.rows) {
|
||||||
const nodeOnline = Number(node.stale_seconds) < 120;
|
const nodeOnline = Number(node.stale_seconds) < 120;
|
||||||
|
|
@ -222,79 +235,51 @@ router.get('/devices/blackmagic/signal', async (req, res, next) => {
|
||||||
const model = (node.capabilities && node.capabilities.blackmagic_model) || null;
|
const model = (node.capabilities && node.capabilities.blackmagic_model) || null;
|
||||||
const localHostname = process.env.NODE_HOSTNAME || '';
|
const localHostname = process.env.NODE_HOSTNAME || '';
|
||||||
const isRemote = node.api_url && node.hostname !== localHostname;
|
const isRemote = node.api_url && node.hostname !== localHostname;
|
||||||
|
|
||||||
bm.forEach((d, idx) => {
|
bm.forEach((d, idx) => {
|
||||||
const portIndex = d.index !== undefined ? d.index : idx;
|
const portIndex = d.index !== undefined ? d.index : idx;
|
||||||
const rec = recByPort.get(`${node.id}:${portIndex}`);
|
const rec = recByPort.get(`${node.id}:${portIndex}`);
|
||||||
|
|
||||||
tasks.push((async () => {
|
tasks.push((async () => {
|
||||||
const base = {
|
const base = {
|
||||||
node_id: node.id,
|
node_id: node.id, hostname: node.hostname, index: portIndex,
|
||||||
hostname: node.hostname,
|
device: d.device || null, model, node_online: nodeOnline,
|
||||||
index: portIndex,
|
recorder_id: rec ? rec.id : null, recorder_name: rec ? rec.name : null,
|
||||||
device: d.device || null,
|
|
||||||
model,
|
|
||||||
node_online: nodeOnline,
|
|
||||||
recorder_id: rec ? rec.id : null,
|
|
||||||
recorder_name: rec ? rec.name : null,
|
|
||||||
recorder_status: rec ? rec.status : null,
|
recorder_status: rec ? rec.status : null,
|
||||||
signal: 'no-recorder',
|
signal: 'no-recorder', framesReceived: null, currentFps: null,
|
||||||
framesReceived: null,
|
|
||||||
currentFps: null,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!rec || rec.status !== 'recording' || !rec.container_id) {
|
if (!rec || rec.status !== 'recording' || !rec.container_id) {
|
||||||
// No active capture — if there's a recorder but it's not recording,
|
|
||||||
// report that; otherwise the port is unassigned.
|
|
||||||
if (rec && rec.status !== 'recording') base.signal = 'idle';
|
if (rec && rec.status !== 'recording') base.signal = 'idle';
|
||||||
return base;
|
return base;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Active recording — query the capture container for real signal.
|
|
||||||
try {
|
try {
|
||||||
let live = null;
|
let live = null;
|
||||||
if (isRemote) {
|
if (isRemote) {
|
||||||
const r = await fetch(
|
const r = await fetch(`${node.api_url}/sidecar/${rec.container_id}/status`, { signal: AbortSignal.timeout(2500) });
|
||||||
`${node.api_url}/sidecar/${rec.container_id}/status`,
|
|
||||||
{ signal: AbortSignal.timeout(2500) }
|
|
||||||
);
|
|
||||||
if (r.ok) live = (await r.json()).live;
|
if (r.ok) live = (await r.json()).live;
|
||||||
} else {
|
} else {
|
||||||
const r = await fetch(
|
const r = await fetch(`http://recorder-${rec.id}:3001/capture/status`, { signal: AbortSignal.timeout(2000) });
|
||||||
`http://recorder-${rec.id}:3001/capture/status`,
|
|
||||||
{ signal: AbortSignal.timeout(2000) }
|
|
||||||
);
|
|
||||||
if (r.ok) live = await r.json();
|
if (r.ok) live = await r.json();
|
||||||
}
|
}
|
||||||
if (live && live.signal) {
|
if (live && live.signal) {
|
||||||
base.signal = live.signal;
|
base.signal = live.signal;
|
||||||
base.framesReceived = live.framesReceived ?? null;
|
base.framesReceived = live.framesReceived ?? null;
|
||||||
base.currentFps = live.currentFps ?? null;
|
base.currentFps = live.currentFps ?? null;
|
||||||
} else {
|
} else { base.signal = 'connecting'; }
|
||||||
base.signal = 'connecting';
|
} catch (_) { base.signal = 'connecting'; }
|
||||||
}
|
|
||||||
} catch (_) {
|
|
||||||
base.signal = 'connecting';
|
|
||||||
}
|
|
||||||
return base;
|
return base;
|
||||||
})());
|
})());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = await Promise.all(tasks);
|
const results = await Promise.all(tasks);
|
||||||
res.json(results);
|
res.json(results);
|
||||||
} catch (err) { next(err); }
|
} catch (err) { next(err); }
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /devices/blackmagic – flatten every node's DeckLink cards for the
|
|
||||||
// recorder picker. Returns one entry per device with the host node info.
|
|
||||||
router.get('/devices/blackmagic', async (req, res, next) => {
|
router.get('/devices/blackmagic', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const r = await pool.query(
|
const r = await pool.query(
|
||||||
`SELECT id, hostname, ip_address, role, capabilities,
|
`SELECT id, hostname, ip_address, role, capabilities,
|
||||||
EXTRACT(EPOCH FROM (NOW() - last_seen)) AS stale_seconds
|
EXTRACT(EPOCH FROM (NOW() - last_seen)) AS stale_seconds
|
||||||
FROM cluster_nodes
|
FROM cluster_nodes WHERE capabilities IS NOT NULL`
|
||||||
WHERE capabilities IS NOT NULL`
|
|
||||||
);
|
);
|
||||||
const out = [];
|
const out = [];
|
||||||
for (const row of r.rows) {
|
for (const row of r.rows) {
|
||||||
|
|
@ -302,157 +287,98 @@ router.get('/devices/blackmagic', async (req, res, next) => {
|
||||||
const bm = (row.capabilities && row.capabilities.blackmagic) || [];
|
const bm = (row.capabilities && row.capabilities.blackmagic) || [];
|
||||||
const model = (row.capabilities && row.capabilities.blackmagic_model) || null;
|
const model = (row.capabilities && row.capabilities.blackmagic_model) || null;
|
||||||
bm.forEach((d, idx) => {
|
bm.forEach((d, idx) => {
|
||||||
out.push({
|
out.push({ node_id: row.id, hostname: row.hostname, ip_address: row.ip_address,
|
||||||
node_id: row.id,
|
role: row.role, online, model, index: d.index !== undefined ? d.index : idx, device: d.device });
|
||||||
hostname: row.hostname,
|
|
||||||
ip_address: row.ip_address,
|
|
||||||
role: row.role,
|
|
||||||
online,
|
|
||||||
model,
|
|
||||||
index: d.index !== undefined ? d.index : idx,
|
|
||||||
device: d.device,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
res.json(out);
|
res.json(out);
|
||||||
} catch (err) { next(err); }
|
} catch (err) { next(err); }
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /devices/deltacast – flatten every node's Deltacast cards for the
|
|
||||||
// recorder picker. Mirrors /devices/blackmagic shape so the UI can treat
|
|
||||||
// both card types uniformly.
|
|
||||||
router.get('/devices/deltacast', async (req, res, next) => {
|
router.get('/devices/deltacast', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const r = await pool.query(
|
const r = await pool.query(
|
||||||
`SELECT id, hostname, ip_address, role, capabilities,
|
`SELECT id, hostname, ip_address, role, capabilities,
|
||||||
EXTRACT(EPOCH FROM (NOW() - last_seen)) AS stale_seconds
|
EXTRACT(EPOCH FROM (NOW() - last_seen)) AS stale_seconds
|
||||||
FROM cluster_nodes
|
FROM cluster_nodes WHERE capabilities IS NOT NULL`
|
||||||
WHERE capabilities IS NOT NULL`
|
|
||||||
);
|
);
|
||||||
const out = [];
|
const out = [];
|
||||||
for (const row of r.rows) {
|
for (const row of r.rows) {
|
||||||
const online = Number(row.stale_seconds) < 120;
|
const online = Number(row.stale_seconds) < 120;
|
||||||
const dc = (row.capabilities && row.capabilities.deltacast) || [];
|
const dc = (row.capabilities && row.capabilities.deltacast) || [];
|
||||||
const model = (row.capabilities && row.capabilities.deltacast_model) || null;
|
const model = (row.capabilities && row.capabilities.deltacast_model) || null;
|
||||||
// Also synthesise entries from DELTACAST_PORT_COUNT if no entries reported yet —
|
|
||||||
// useful for nodes that haven't sent a heartbeat since the agent was updated.
|
|
||||||
dc.forEach((d, idx) => {
|
dc.forEach((d, idx) => {
|
||||||
out.push({
|
out.push({ node_id: row.id, hostname: row.hostname, ip_address: row.ip_address,
|
||||||
node_id: row.id,
|
role: row.role, online, model: model || 'Deltacast',
|
||||||
hostname: row.hostname,
|
index: d.index !== undefined ? d.index : idx, device: d.device,
|
||||||
ip_address: row.ip_address,
|
present: d.present !== false, port_count: dc.length });
|
||||||
role: row.role,
|
|
||||||
online,
|
|
||||||
model: model || 'Deltacast',
|
|
||||||
index: d.index !== undefined ? d.index : idx,
|
|
||||||
device: d.device,
|
|
||||||
present: d.present !== false,
|
|
||||||
port_count: dc.length,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
res.json(out);
|
res.json(out);
|
||||||
} catch (err) { next(err); }
|
} catch (err) { next(err); }
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /devices/deltacast/signal – live signal state for Deltacast ports.
|
|
||||||
// Same pattern as /devices/blackmagic/signal.
|
|
||||||
router.get('/devices/deltacast/signal', async (req, res, next) => {
|
router.get('/devices/deltacast/signal', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const [nodesRes, recordersRes] = await Promise.all([
|
const [nodesRes, recordersRes] = await Promise.all([
|
||||||
pool.query(
|
pool.query(`SELECT id, hostname, ip_address, api_url, capabilities,
|
||||||
`SELECT id, hostname, ip_address, api_url, capabilities,
|
|
||||||
EXTRACT(EPOCH FROM (NOW() - last_seen)) AS stale_seconds
|
EXTRACT(EPOCH FROM (NOW() - last_seen)) AS stale_seconds
|
||||||
FROM cluster_nodes
|
FROM cluster_nodes WHERE capabilities IS NOT NULL`),
|
||||||
WHERE capabilities IS NOT NULL`
|
pool.query(`SELECT id, node_id, device_index, status, source_type, container_id
|
||||||
),
|
FROM recorders WHERE source_type = 'deltacast'`),
|
||||||
pool.query(
|
|
||||||
`SELECT id, node_id, device_index, status, source_type, container_id
|
|
||||||
FROM recorders WHERE source_type = 'deltacast'`
|
|
||||||
),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const recByNodePort = {};
|
const recByNodePort = {};
|
||||||
for (const rec of recordersRes.rows) {
|
for (const rec of recordersRes.rows) {
|
||||||
recByNodePort[`${rec.node_id}:${rec.device_index}`] = rec;
|
recByNodePort[`${rec.node_id}:${rec.device_index}`] = rec;
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = [];
|
const results = [];
|
||||||
const fetchPromises = [];
|
const fetchPromises = [];
|
||||||
|
|
||||||
for (const node of nodesRes.rows) {
|
for (const node of nodesRes.rows) {
|
||||||
const online = Number(node.stale_seconds) < 120;
|
const online = Number(node.stale_seconds) < 120;
|
||||||
const dc = (node.capabilities && node.capabilities.deltacast) || [];
|
const dc = (node.capabilities && node.capabilities.deltacast) || [];
|
||||||
const model = (node.capabilities && node.capabilities.deltacast_model) || 'Deltacast';
|
const model = (node.capabilities && node.capabilities.deltacast_model) || 'Deltacast';
|
||||||
|
|
||||||
for (const port of dc) {
|
for (const port of dc) {
|
||||||
const idx = port.index !== undefined ? port.index : dc.indexOf(port);
|
const idx = port.index !== undefined ? port.index : dc.indexOf(port);
|
||||||
const rec = recByNodePort[`${node.id}:${idx}`];
|
const rec = recByNodePort[`${node.id}:${idx}`];
|
||||||
const base = {
|
const base = { node_id: node.id, hostname: node.hostname, ip_address: node.ip_address,
|
||||||
node_id: node.id,
|
online, model, index: idx, device: port.device, present: port.present !== false,
|
||||||
hostname: node.hostname,
|
recorder_id: rec ? rec.id : null, recorder_status: rec ? rec.status : null,
|
||||||
ip_address: node.ip_address,
|
signal: 'no-recorder', framesReceived: null, currentFps: null };
|
||||||
online,
|
|
||||||
model,
|
|
||||||
index: idx,
|
|
||||||
device: port.device,
|
|
||||||
present: port.present !== false,
|
|
||||||
recorder_id: rec ? rec.id : null,
|
|
||||||
recorder_status: rec ? rec.status : null,
|
|
||||||
signal: 'no-recorder',
|
|
||||||
framesReceived: null,
|
|
||||||
currentFps: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!rec) { results.push(base); continue; }
|
if (!rec) { results.push(base); continue; }
|
||||||
if (rec.status !== 'recording') { base.signal = 'idle'; results.push(base); continue; }
|
if (rec.status !== 'recording') { base.signal = 'idle'; results.push(base); continue; }
|
||||||
|
|
||||||
// Active recording — query capture container for real signal.
|
|
||||||
const fetchIdx = results.length;
|
const fetchIdx = results.length;
|
||||||
results.push(base);
|
results.push(base);
|
||||||
fetchPromises.push((async () => {
|
fetchPromises.push((async () => {
|
||||||
try {
|
try {
|
||||||
const url = node.api_url
|
const url = node.api_url ? `${node.api_url}/sidecar/${rec.container_id}/status`
|
||||||
? `${node.api_url}/sidecar/${rec.container_id}/status`
|
|
||||||
: `http://recorder-${rec.id}:3001/capture/status`;
|
: `http://recorder-${rec.id}:3001/capture/status`;
|
||||||
const r = await fetch(url, { signal: AbortSignal.timeout(2500) });
|
const r = await fetch(url, { signal: AbortSignal.timeout(2500) });
|
||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
const live = await r.json();
|
const live = await r.json();
|
||||||
if (live && live.signal) {
|
if (live && live.signal) {
|
||||||
results[fetchIdx].signal = live.signal;
|
results[fetchIdx].signal = live.signal;
|
||||||
results[fetchIdx].framesReceived = live.framesReceived ?? null;
|
results[fetchIdx].framesReceived = live.framesReceived ?? null;
|
||||||
results[fetchIdx].currentFps = live.currentFps ?? null;
|
results[fetchIdx].currentFps = live.currentFps ?? null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (_) {
|
} catch (_) { results[fetchIdx].signal = 'connecting'; }
|
||||||
results[fetchIdx].signal = 'connecting';
|
|
||||||
}
|
|
||||||
})());
|
})());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(fetchPromises);
|
await Promise.all(fetchPromises);
|
||||||
res.json(results);
|
res.json(results);
|
||||||
} catch (err) { next(err); }
|
} catch (err) { next(err); }
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /:id/ping – probe the node's api_url/health endpoint directly
|
|
||||||
router.get('/:id/ping', async (req, res, next) => {
|
router.get('/:id/ping', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const r = await pool.query(
|
const r = await pool.query('SELECT id, hostname, api_url FROM cluster_nodes WHERE id = $1', [req.params.id]);
|
||||||
'SELECT id, hostname, api_url FROM cluster_nodes WHERE id = $1',
|
|
||||||
[req.params.id]
|
|
||||||
);
|
|
||||||
if (r.rowCount === 0) return res.status(404).json({ error: 'Node not found' });
|
if (r.rowCount === 0) return res.status(404).json({ error: 'Node not found' });
|
||||||
|
|
||||||
const node = r.rows[0];
|
const node = r.rows[0];
|
||||||
if (!node.api_url) return res.json({ reachable: false, reason: 'no api_url registered' });
|
if (!node.api_url) return res.json({ reachable: false, reason: 'no api_url registered' });
|
||||||
|
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
try {
|
try {
|
||||||
const upstream = await fetch(`${node.api_url}/health`, {
|
const upstream = await fetch(`${node.api_url}/health`, { signal: AbortSignal.timeout(4000) });
|
||||||
signal: AbortSignal.timeout(4000),
|
|
||||||
});
|
|
||||||
const latency_ms = Date.now() - start;
|
const latency_ms = Date.now() - start;
|
||||||
const body = await upstream.json().catch(() => ({}));
|
const body = await upstream.json().catch(() => ({}));
|
||||||
res.json({ reachable: upstream.ok, latency_ms, status: upstream.status, agent: body });
|
res.json({ reachable: upstream.ok, latency_ms, status: upstream.status, agent: body });
|
||||||
|
|
@ -462,8 +388,83 @@ router.get('/:id/ping', async (req, res, next) => {
|
||||||
} catch (err) { next(err); }
|
} catch (err) { next(err); }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Capture-driver / SDK deployment ────────────────────────────────────────
|
||||||
|
// Admins install/update vendor capture-card drivers on a node from the UI.
|
||||||
|
// We resolve the node's api_url (like /:id/ping) and forward to its node-agent,
|
||||||
|
// which runs deploy/install-driver.sh <vendor> in a privileged one-shot
|
||||||
|
// container against the host kernel. Vendor is allowlisted here AND on the
|
||||||
|
// agent. We never echo the agent token or proprietary paths back to the client.
|
||||||
|
const DRIVER_VENDORS = ['blackmagic', 'aja', 'deltacast', 'ndi'];
|
||||||
|
|
||||||
|
// Bearer the agent expects (its NODE_TOKEN). Configured server-side; never
|
||||||
|
// derived from client input and never returned to the browser.
|
||||||
|
function agentAuthHeaders() {
|
||||||
|
const tok = process.env.NODE_AGENT_TOKEN || '';
|
||||||
|
return tok ? { Authorization: `Bearer ${tok}` } : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveNode(id) {
|
||||||
|
const r = await pool.query('SELECT id, hostname, api_url, capabilities FROM cluster_nodes WHERE id = $1', [id]);
|
||||||
|
return r.rowCount === 0 ? null : r.rows[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
router.get('/:id/driver-status', requireAdmin, async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const node = await resolveNode(req.params.id);
|
||||||
|
if (!node) return res.status(404).json({ error: 'Node not found' });
|
||||||
|
if (!node.api_url) return res.status(409).json({ error: 'Node has no api_url registered' });
|
||||||
|
try {
|
||||||
|
const upstream = await fetch(`${node.api_url}/driver/status`, {
|
||||||
|
headers: agentAuthHeaders(),
|
||||||
|
signal: AbortSignal.timeout(6000),
|
||||||
|
});
|
||||||
|
const body = await upstream.json().catch(() => ({}));
|
||||||
|
if (!upstream.ok) {
|
||||||
|
return res.status(502).json({ error: 'Agent driver-status failed', status: upstream.status });
|
||||||
|
}
|
||||||
|
res.json(body);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(502).json({ error: 'Node unreachable', reason: err.message });
|
||||||
|
}
|
||||||
|
} catch (err) { next(err); }
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/:id/install-driver', requireAdmin, async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const vendor = String(req.body?.vendor || '').toLowerCase();
|
||||||
|
if (!DRIVER_VENDORS.includes(vendor)) {
|
||||||
|
return res.status(400).json({ error: `Invalid vendor (allowed: ${DRIVER_VENDORS.join(', ')})` });
|
||||||
|
}
|
||||||
|
const node = await resolveNode(req.params.id);
|
||||||
|
if (!node) return res.status(404).json({ error: 'Node not found' });
|
||||||
|
if (!node.api_url) return res.status(409).json({ error: 'Node has no api_url registered' });
|
||||||
|
|
||||||
|
try {
|
||||||
|
// DKMS builds can take minutes — generous timeout.
|
||||||
|
const upstream = await fetch(`${node.api_url}/driver/install`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', ...agentAuthHeaders() },
|
||||||
|
body: JSON.stringify({ vendor }),
|
||||||
|
signal: AbortSignal.timeout(600000),
|
||||||
|
});
|
||||||
|
const body = await upstream.json().catch(() => ({}));
|
||||||
|
// Relay logs/result. install-driver.sh never echoes secrets; the agent
|
||||||
|
// returns only its structured [install-driver] log lines + status.
|
||||||
|
res.status(upstream.ok ? 200 : 502).json({
|
||||||
|
ok: !!body.ok,
|
||||||
|
vendor,
|
||||||
|
exitCode: body.exitCode ?? null,
|
||||||
|
rebootRequired: !!body.rebootRequired,
|
||||||
|
status: body.status ?? null,
|
||||||
|
logs: typeof body.logs === 'string' ? body.logs : '',
|
||||||
|
error: body.ok ? undefined : (body.error || 'Install failed — see logs'),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
res.status(502).json({ error: 'Node unreachable or install timed out', reason: err.message });
|
||||||
|
}
|
||||||
|
} catch (err) { next(err); }
|
||||||
|
});
|
||||||
|
|
||||||
// GET /metrics - live per-node utilization (CPU, RAM, GPU)
|
|
||||||
router.get('/metrics', async (req, res, next) => {
|
router.get('/metrics', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const r = await pool.query(
|
const r = await pool.query(
|
||||||
|
|
@ -471,59 +472,37 @@ router.get('/metrics', async (req, res, next) => {
|
||||||
cpu_usage, mem_used_mb, mem_total_mb,
|
cpu_usage, mem_used_mb, mem_total_mb,
|
||||||
capabilities, metrics,
|
capabilities, metrics,
|
||||||
EXTRACT(EPOCH FROM (NOW() - last_seen)) AS stale_seconds
|
EXTRACT(EPOCH FROM (NOW() - last_seen)) AS stale_seconds
|
||||||
FROM cluster_nodes
|
FROM cluster_nodes ORDER BY registered_at ASC`
|
||||||
ORDER BY registered_at ASC`
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const nodes = r.rows.map(row => {
|
const nodes = r.rows.map(row => {
|
||||||
const capGpus = (row.capabilities && row.capabilities.gpus) || [];
|
const capGpus = (row.capabilities && row.capabilities.gpus) || [];
|
||||||
const liveGpus = (row.metrics && row.metrics.gpus) || [];
|
const liveGpus = (row.metrics && row.metrics.gpus) || [];
|
||||||
|
|
||||||
const gpus = capGpus.map((g, idx) => {
|
const gpus = capGpus.map((g, idx) => {
|
||||||
const live = liveGpus.find(l => l.index === g.index) || liveGpus[idx] || {};
|
const live = liveGpus.find(l => l.index === g.index) || liveGpus[idx] || {};
|
||||||
return {
|
return { name: g.name || null, util_pct: live.util_pct != null ? live.util_pct : null,
|
||||||
name: g.name || null,
|
memory_used_mb: live.memory_used_mb != null ? live.memory_used_mb : null,
|
||||||
util_pct: live.util_pct != null ? live.util_pct : null,
|
memory_total_mb: g.memory_mb != null ? g.memory_mb : (live.memory_total_mb ?? null) };
|
||||||
memory_used_mb: live.memory_used_mb != null ? live.memory_used_mb : null,
|
|
||||||
memory_total_mb: g.memory_mb != null ? g.memory_mb : (live.memory_total_mb ?? null),
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
// include any live GPUs not in static capabilities
|
|
||||||
for (const lg of liveGpus) {
|
for (const lg of liveGpus) {
|
||||||
if (!capGpus.some(g => g.index === lg.index)) {
|
if (!capGpus.some(g => g.index === lg.index)) {
|
||||||
gpus.push({
|
gpus.push({ name: lg.name || null, util_pct: lg.util_pct != null ? lg.util_pct : null,
|
||||||
name: lg.name || null,
|
memory_used_mb: lg.memory_used_mb != null ? lg.memory_used_mb : null,
|
||||||
util_pct: lg.util_pct != null ? lg.util_pct : null,
|
memory_total_mb: lg.memory_total_mb != null ? lg.memory_total_mb : null });
|
||||||
memory_used_mb: lg.memory_used_mb != null ? lg.memory_used_mb : null,
|
|
||||||
memory_total_mb: lg.memory_total_mb != null ? lg.memory_total_mb : null,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return { id: row.id, hostname: row.hostname, role: row.role,
|
||||||
return {
|
online: Number(row.stale_seconds) < 120, last_seen: row.last_seen,
|
||||||
id: row.id,
|
cpu_util_pct: row.cpu_usage != null ? Number(row.cpu_usage) : null,
|
||||||
hostname: row.hostname,
|
ram_used_mb: row.mem_used_mb != null ? row.mem_used_mb : null,
|
||||||
role: row.role,
|
ram_total_mb: row.mem_total_mb != null ? row.mem_total_mb : null, gpus };
|
||||||
online: Number(row.stale_seconds) < 120,
|
|
||||||
last_seen: row.last_seen,
|
|
||||||
cpu_util_pct: row.cpu_usage != null ? Number(row.cpu_usage) : null,
|
|
||||||
ram_used_mb: row.mem_used_mb != null ? row.mem_used_mb : null,
|
|
||||||
ram_total_mb: row.mem_total_mb != null ? row.mem_total_mb : null,
|
|
||||||
gpus,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({ nodes });
|
res.json({ nodes });
|
||||||
} catch (err) { next(err); }
|
} catch (err) { next(err); }
|
||||||
});
|
});
|
||||||
|
|
||||||
// DELETE /:id – deregister a node
|
|
||||||
router.delete('/:id', async (req, res, next) => {
|
router.delete('/:id', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const r = await pool.query(
|
const r = await pool.query('DELETE FROM cluster_nodes WHERE id = $1 RETURNING id', [req.params.id]);
|
||||||
'DELETE FROM cluster_nodes WHERE id = $1 RETURNING id',
|
|
||||||
[req.params.id]
|
|
||||||
);
|
|
||||||
if (r.rowCount === 0) return res.status(404).json({ error: 'Node not found' });
|
if (r.rowCount === 0) return res.status(404).json({ error: 'Node not found' });
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
} catch (err) { next(err); }
|
} catch (err) { next(err); }
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@ import {
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// ── BullMQ: media staging queue (S3 -> /media volume) ────────────────────────
|
|
||||||
const parseRedisUrl = (url) => {
|
const parseRedisUrl = (url) => {
|
||||||
const parsed = new URL(url);
|
const parsed = new URL(url);
|
||||||
return { host: parsed.hostname, port: parseInt(parsed.port, 10) };
|
return { host: parsed.hostname, port: parseInt(parsed.port, 10) };
|
||||||
|
|
@ -30,7 +29,6 @@ const stageQueue = new Queue('playout-stage', {
|
||||||
connection: parseRedisUrl(process.env.REDIS_URL || 'redis://queue:6379'),
|
connection: parseRedisUrl(process.env.REDIS_URL || 'redis://queue:6379'),
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Sidecar orchestration (mirrors recorders.js) ─────────────────────────────
|
|
||||||
const PLAYOUT_SIDECAR_IMAGE = process.env.PLAYOUT_IMAGE || 'wild-dragon-playout:latest';
|
const PLAYOUT_SIDECAR_IMAGE = process.env.PLAYOUT_IMAGE || 'wild-dragon-playout:latest';
|
||||||
|
|
||||||
function dockerApi(method, path, body = null) {
|
function dockerApi(method, path, body = null) {
|
||||||
|
|
@ -68,16 +66,10 @@ async function resolveNodeTarget(nodeId) {
|
||||||
return { remote: true, apiUrl: node.api_url, ip: node.ip_address };
|
return { remote: true, apiUrl: node.api_url, ip: node.ip_address };
|
||||||
}
|
}
|
||||||
|
|
||||||
// The sidecar shim listens on this port inside the container. The mam-api talks
|
|
||||||
// to it by container alias on the shared docker network (local) or via the
|
|
||||||
// node-agent's returned host:port (remote).
|
|
||||||
const SIDECAR_HTTP_PORT = 3002;
|
const SIDECAR_HTTP_PORT = 3002;
|
||||||
|
|
||||||
function channelAlias(id) { return `playout-${id}`; }
|
function channelAlias(id) { return `playout-${id}`; }
|
||||||
|
|
||||||
// Resolve the base URL the API uses to reach a running channel's sidecar shim.
|
|
||||||
// Local: the docker-network alias. Remote: the node-agent reported the host the
|
|
||||||
// container is published on (stored in container_meta.sidecar_url).
|
|
||||||
function sidecarBaseUrl(channel) {
|
function sidecarBaseUrl(channel) {
|
||||||
if (channel.container_meta && channel.container_meta.sidecar_url) {
|
if (channel.container_meta && channel.container_meta.sidecar_url) {
|
||||||
return channel.container_meta.sidecar_url;
|
return channel.container_meta.sidecar_url;
|
||||||
|
|
@ -100,7 +92,6 @@ async function callSidecar(channel, path, method = 'POST', body = null) {
|
||||||
return res.json().catch(() => ({}));
|
return res.json().catch(() => ({}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Serialization ────────────────────────────────────────────────────────────
|
|
||||||
function channelToJson(r) {
|
function channelToJson(r) {
|
||||||
return {
|
return {
|
||||||
id: r.id,
|
id: r.id,
|
||||||
|
|
@ -123,7 +114,6 @@ function channelToJson(r) {
|
||||||
|
|
||||||
const OUTPUT_TYPES = new Set(['decklink', 'ndi', 'srt', 'rtmp']);
|
const OUTPUT_TYPES = new Set(['decklink', 'ndi', 'srt', 'rtmp']);
|
||||||
|
|
||||||
// ── Param resolver: scope every /:id route to the channel's project ──────────
|
|
||||||
router.param('id', async (req, res, next) => {
|
router.param('id', async (req, res, next) => {
|
||||||
validateUuid('id')(req, res, () => {});
|
validateUuid('id')(req, res, () => {});
|
||||||
if (res.headersSent) return;
|
if (res.headersSent) return;
|
||||||
|
|
@ -143,9 +133,6 @@ async function requireChannelEdit(req, res, next) {
|
||||||
catch (err) { next(err); }
|
catch (err) { next(err); }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Channels ─────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
// GET /playout/channels — list (filtered to accessible projects)
|
|
||||||
router.get('/channels', async (req, res, next) => {
|
router.get('/channels', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
let rows;
|
let rows;
|
||||||
|
|
@ -162,7 +149,6 @@ router.get('/channels', async (req, res, next) => {
|
||||||
} catch (err) { next(err); }
|
} catch (err) { next(err); }
|
||||||
});
|
});
|
||||||
|
|
||||||
// POST /playout/channels — create
|
|
||||||
router.post('/channels', async (req, res, next) => {
|
router.post('/channels', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { name, node_id = null, output_type = 'srt', output_config = {},
|
const { name, node_id = null, output_type = 'srt', output_config = {},
|
||||||
|
|
@ -173,8 +159,6 @@ router.post('/channels', async (req, res, next) => {
|
||||||
if (!OUTPUT_TYPES.has(output_type)) {
|
if (!OUTPUT_TYPES.has(output_type)) {
|
||||||
return res.status(400).json({ error: `output_type must be one of: ${[...OUTPUT_TYPES].join(', ')}` });
|
return res.status(400).json({ error: `output_type must be one of: ${[...OUTPUT_TYPES].join(', ')}` });
|
||||||
}
|
}
|
||||||
// Creating a project-scoped channel requires edit on that project; a
|
|
||||||
// null-project (admin-only) channel requires admin.
|
|
||||||
if (project_id) await assertProjectAccess(req.user, project_id, 'edit');
|
if (project_id) await assertProjectAccess(req.user, project_id, 'edit');
|
||||||
else if (!isAdmin(req.user)) return res.status(403).json({ error: 'admin required for unassigned channel' });
|
else if (!isAdmin(req.user)) return res.status(403).json({ error: 'admin required for unassigned channel' });
|
||||||
|
|
||||||
|
|
@ -187,7 +171,6 @@ router.post('/channels', async (req, res, next) => {
|
||||||
} catch (err) { next(err); }
|
} catch (err) { next(err); }
|
||||||
});
|
});
|
||||||
|
|
||||||
// PATCH /playout/channels/:id — update config (only while stopped)
|
|
||||||
router.patch('/channels/:id', requireChannelEdit, async (req, res, next) => {
|
router.patch('/channels/:id', requireChannelEdit, async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
if (req.channel.status === 'running') {
|
if (req.channel.status === 'running') {
|
||||||
|
|
@ -214,7 +197,6 @@ router.patch('/channels/:id', requireChannelEdit, async (req, res, next) => {
|
||||||
} catch (err) { next(err); }
|
} catch (err) { next(err); }
|
||||||
});
|
});
|
||||||
|
|
||||||
// DELETE /playout/channels/:id
|
|
||||||
router.delete('/channels/:id', requireChannelEdit, async (req, res, next) => {
|
router.delete('/channels/:id', requireChannelEdit, async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
if (req.channel.status === 'running') {
|
if (req.channel.status === 'running') {
|
||||||
|
|
@ -225,14 +207,9 @@ router.delete('/channels/:id', requireChannelEdit, async (req, res, next) => {
|
||||||
} catch (err) { next(err); }
|
} catch (err) { next(err); }
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Port-contention guard (DeckLink) ─────────────────────────────────────────
|
|
||||||
// A DeckLink device on a node is exclusive: an active recorder OR another active
|
|
||||||
// channel on the same node+index blocks a new SDI channel. NDI/SRT/RTMP have no
|
|
||||||
// hardware contention.
|
|
||||||
async function assertDeckLinkFree(channel) {
|
async function assertDeckLinkFree(channel) {
|
||||||
if (channel.output_type !== 'decklink') return;
|
if (channel.output_type !== 'decklink') return;
|
||||||
const idx = (channel.output_config && channel.output_config.device_index) || 1;
|
const idx = (channel.output_config && channel.output_config.device_index) || 1;
|
||||||
// Another running channel on the same node + device index?
|
|
||||||
const chan = await pool.query(
|
const chan = await pool.query(
|
||||||
`SELECT id FROM playout_channels
|
`SELECT id FROM playout_channels
|
||||||
WHERE id <> $1 AND node_id IS NOT DISTINCT FROM $2 AND status = 'running'
|
WHERE id <> $1 AND node_id IS NOT DISTINCT FROM $2 AND status = 'running'
|
||||||
|
|
@ -242,7 +219,6 @@ async function assertDeckLinkFree(channel) {
|
||||||
if (chan.rows.length > 0) {
|
if (chan.rows.length > 0) {
|
||||||
throw Object.assign(new Error(`DeckLink device ${idx} already in use by another channel on this node`), { httpStatus: 409 });
|
throw Object.assign(new Error(`DeckLink device ${idx} already in use by another channel on this node`), { httpStatus: 409 });
|
||||||
}
|
}
|
||||||
// An active recorder using the same device index on the same node?
|
|
||||||
const rec = await pool.query(
|
const rec = await pool.query(
|
||||||
`SELECT id FROM recorders
|
`SELECT id FROM recorders
|
||||||
WHERE node_id IS NOT DISTINCT FROM $1 AND device_index = $2
|
WHERE node_id IS NOT DISTINCT FROM $1 AND device_index = $2
|
||||||
|
|
@ -254,13 +230,6 @@ async function assertDeckLinkFree(channel) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spawn the CasparCG sidecar for a channel and flip it to 'running'. Shared by
|
|
||||||
// the /start route and the scheduler failover path (restartChannel) so neither
|
|
||||||
// duplicates the docker/node-agent orchestration. Caller is responsible for the
|
|
||||||
// pre-flight guards (status check, DeckLink contention) appropriate to its path.
|
|
||||||
//
|
|
||||||
// On any spawn failure the channel is left status='error' with a message and an
|
|
||||||
// Error carrying { httpStatus } is thrown. On success returns the updated row.
|
|
||||||
async function spawnChannelSidecar(channel) {
|
async function spawnChannelSidecar(channel) {
|
||||||
await pool.query('UPDATE playout_channels SET status = $1, error_message = NULL WHERE id = $2', ['starting', channel.id]);
|
await pool.query('UPDATE playout_channels SET status = $1, error_message = NULL WHERE id = $2', ['starting', channel.id]);
|
||||||
|
|
||||||
|
|
@ -269,8 +238,6 @@ async function spawnChannelSidecar(channel) {
|
||||||
`OUTPUT_CONFIG=${JSON.stringify(channel.output_config || {})}`,
|
`OUTPUT_CONFIG=${JSON.stringify(channel.output_config || {})}`,
|
||||||
`VIDEO_FORMAT=${channel.video_format}`,
|
`VIDEO_FORMAT=${channel.video_format}`,
|
||||||
`PORT=${SIDECAR_HTTP_PORT}`,
|
`PORT=${SIDECAR_HTTP_PORT}`,
|
||||||
// Drives the HLS preview path (/media/live/<channel_id>/index.m3u8) and
|
|
||||||
// the per-channel resource naming inside the sidecar.
|
|
||||||
`CHANNEL_ID=${channel.id}`,
|
`CHANNEL_ID=${channel.id}`,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -301,7 +268,6 @@ async function spawnChannelSidecar(channel) {
|
||||||
}
|
}
|
||||||
const data = await sidecarRes.json();
|
const data = await sidecarRes.json();
|
||||||
containerId = data.containerId;
|
containerId = data.containerId;
|
||||||
// node-agent returns the reachable host:port the shim is published on.
|
|
||||||
if (data.sidecarUrl || data.host) {
|
if (data.sidecarUrl || data.host) {
|
||||||
containerMeta.sidecar_url = data.sidecarUrl || `http://${data.host}:${SIDECAR_HTTP_PORT}`;
|
containerMeta.sidecar_url = data.sidecarUrl || `http://${data.host}:${SIDECAR_HTTP_PORT}`;
|
||||||
}
|
}
|
||||||
|
|
@ -314,7 +280,10 @@ async function spawnChannelSidecar(channel) {
|
||||||
Image: PLAYOUT_SIDECAR_IMAGE,
|
Image: PLAYOUT_SIDECAR_IMAGE,
|
||||||
Env: env,
|
Env: env,
|
||||||
HostConfig: {
|
HostConfig: {
|
||||||
Privileged: true,
|
// DeckLink SDI needs raw /dev access (privileged). SRT/NDI/RTMP/HLS run
|
||||||
|
// unprivileged — privileged exposes host GPUs to CasparCG, and the
|
||||||
|
// missing in-container NVIDIA driver crashes the engine within seconds.
|
||||||
|
Privileged: channel.output_type === 'decklink',
|
||||||
NetworkMode: dockerNetwork,
|
NetworkMode: dockerNetwork,
|
||||||
Binds: hostBinds,
|
Binds: hostBinds,
|
||||||
},
|
},
|
||||||
|
|
@ -353,7 +322,6 @@ async function spawnChannelSidecar(channel) {
|
||||||
return rows[0];
|
return rows[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /playout/channels/:id/start — spawn the CasparCG sidecar + bring up output
|
|
||||||
router.post('/channels/:id/start', requireChannelEdit, async (req, res, next) => {
|
router.post('/channels/:id/start', requireChannelEdit, async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const channel = req.channel;
|
const channel = req.channel;
|
||||||
|
|
@ -369,7 +337,6 @@ router.post('/channels/:id/start', requireChannelEdit, async (req, res, next) =>
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// POST /playout/channels/:id/stop — tear down the sidecar
|
|
||||||
router.post('/channels/:id/stop', requireChannelEdit, async (req, res, next) => {
|
router.post('/channels/:id/stop', requireChannelEdit, async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const channel = req.channel;
|
const channel = req.channel;
|
||||||
|
|
@ -394,7 +361,6 @@ router.post('/channels/:id/stop', requireChannelEdit, async (req, res, next) =>
|
||||||
} catch (err) { next(err); }
|
} catch (err) { next(err); }
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /playout/channels/:id/status — live engine status (proxied to sidecar)
|
|
||||||
router.get('/channels/:id/status', async (req, res, next) => {
|
router.get('/channels/:id/status', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
if (req.channel.status !== 'running') {
|
if (req.channel.status !== 'running') {
|
||||||
|
|
@ -448,8 +414,6 @@ async function transport(req, res, action, body = null) {
|
||||||
catch (err) { res.status(502).json({ error: err.message }); }
|
catch (err) { res.status(502).json({ error: err.message }); }
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /playout/channels/:id/play — resolve the channel's playlist, stage-check,
|
|
||||||
// and hand the engine the ordered list of ready clips.
|
|
||||||
router.post('/channels/:id/play', requireChannelEdit, async (req, res, next) => {
|
router.post('/channels/:id/play', requireChannelEdit, async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
if (req.channel.status !== 'running') {
|
if (req.channel.status !== 'running') {
|
||||||
|
|
@ -503,7 +467,6 @@ router.post('/channels/:id/resume', requireChannelEdit, (req, res) => transport(
|
||||||
router.post('/channels/:id/skip', requireChannelEdit, (req, res) => transport(req, res, '/transport/skip'));
|
router.post('/channels/:id/skip', requireChannelEdit, (req, res) => transport(req, res, '/transport/skip'));
|
||||||
router.post('/channels/:id/stop-playback', requireChannelEdit, (req, res) => transport(req, res, '/channel/stop'));
|
router.post('/channels/:id/stop-playback', requireChannelEdit, (req, res) => transport(req, res, '/channel/stop'));
|
||||||
|
|
||||||
// GET /playout/channels/:id/asrun — as-run log
|
|
||||||
router.get('/channels/:id/asrun', async (req, res, next) => {
|
router.get('/channels/:id/asrun', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { rows } = await pool.query(
|
const { rows } = await pool.query(
|
||||||
|
|
@ -513,10 +476,118 @@ router.get('/channels/:id/asrun', async (req, res, next) => {
|
||||||
} catch (err) { next(err); }
|
} catch (err) { next(err); }
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Playlists ────────────────────────────────────────────────────────────────
|
// ── SCTE-35 ad-break splices ───────────────────────────────────────────────
|
||||||
|
// Schedule, trigger, and list SCTE-35 ad breaks on a channel. A break can be
|
||||||
|
// scheduled (after a playlist position, or at a wall-clock time) or fired
|
||||||
|
// immediately. Firing tells the sidecar to splice the live output, marks the
|
||||||
|
// break 'fired', and stamps a row in the as-run compliance log.
|
||||||
|
const SCTE_TYPES = new Set(['splice_insert', 'immediate', 'splice_out', 'splice_in']);
|
||||||
|
|
||||||
|
// Fire a break row on the sidecar + record it. Shared by the immediate-trigger
|
||||||
|
// route and the scheduler's due-break sweep. Best-effort: a sidecar failure
|
||||||
|
// marks the break 'error' via error_message but never throws to the caller's
|
||||||
|
// HTTP path beyond what's handled here.
|
||||||
|
export async function fireScteBreak(channel, brk) {
|
||||||
|
const out = await callSidecar(channel, '/scte/trigger', 'POST', {
|
||||||
|
eventId: brk.event_id,
|
||||||
|
type: brk.type === 'immediate' ? 'splice_insert' : brk.type,
|
||||||
|
durationS: brk.duration_s,
|
||||||
|
});
|
||||||
|
await pool.query(
|
||||||
|
`UPDATE playout_scte_breaks SET status = 'fired', fired_at = NOW(), updated_at = NOW() WHERE id = $1`,
|
||||||
|
[brk.id]
|
||||||
|
);
|
||||||
|
// Stamp the compliance log. ended_at/duration are known up front for a
|
||||||
|
// fixed-duration break, so the row is written closed.
|
||||||
|
await pool.query(
|
||||||
|
`INSERT INTO playout_as_run
|
||||||
|
(channel_id, item_id, clip_name, started_at, ended_at, duration_s, result)
|
||||||
|
VALUES ($1, $2, $3, NOW(),
|
||||||
|
CASE WHEN $4 > 0 THEN NOW() + ($4 || ' seconds')::interval ELSE NULL END,
|
||||||
|
$4, 'scte')`,
|
||||||
|
[channel.id, brk.id, `SCTE-35 ${brk.type} (${brk.duration_s}s)`, brk.duration_s]
|
||||||
|
);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
router.get('/channels/:id/scte', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { rows } = await pool.query(
|
||||||
|
`SELECT * FROM playout_scte_breaks WHERE channel_id = $1 ORDER BY created_at DESC LIMIT 200`,
|
||||||
|
[req.channel.id]);
|
||||||
|
res.json(rows);
|
||||||
|
} catch (err) { next(err); }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Schedule a break. Body: { type, duration_s, playlist_pos?, scheduled_at? }.
|
||||||
|
// A pending break with a playlist_pos / scheduled_at is fired later by the
|
||||||
|
// scheduler; one with neither is fired immediately for convenience.
|
||||||
|
router.post('/channels/:id/scte', requireChannelEdit, async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { type = 'splice_insert', duration_s = 30,
|
||||||
|
playlist_pos = null, scheduled_at = null } = req.body || {};
|
||||||
|
if (!SCTE_TYPES.has(type)) {
|
||||||
|
return res.status(400).json({ error: `type must be one of: ${[...SCTE_TYPES].join(', ')}` });
|
||||||
|
}
|
||||||
|
const dur = Math.max(0, parseInt(duration_s, 10) || 0);
|
||||||
|
// Auto-assign a monotonically increasing splice_event_id per channel.
|
||||||
|
const ev = await pool.query(
|
||||||
|
`SELECT COALESCE(MAX(event_id), 0) + 1 AS next FROM playout_scte_breaks WHERE channel_id = $1`,
|
||||||
|
[req.channel.id]);
|
||||||
|
const eventId = ev.rows[0].next;
|
||||||
|
const { rows } = await pool.query(
|
||||||
|
`INSERT INTO playout_scte_breaks
|
||||||
|
(channel_id, playlist_pos, scheduled_at, duration_s, event_id, type, created_by)
|
||||||
|
VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING *`,
|
||||||
|
[req.channel.id, playlist_pos, scheduled_at, dur, eventId, type, req.user?.id || null]);
|
||||||
|
res.status(201).json(rows[0]);
|
||||||
|
} catch (err) { next(err); }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fire an ad break immediately ("splice now"). Body: { type?, duration_s? }.
|
||||||
|
// Creates the break row and triggers the splice on the live output in one shot.
|
||||||
|
router.post('/channels/:id/scte/trigger', requireChannelEdit, async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
if (req.channel.status !== 'running') {
|
||||||
|
return res.status(409).json({ error: 'Channel is not running' });
|
||||||
|
}
|
||||||
|
const { type = 'immediate', duration_s = 30 } = req.body || {};
|
||||||
|
if (!SCTE_TYPES.has(type)) {
|
||||||
|
return res.status(400).json({ error: `type must be one of: ${[...SCTE_TYPES].join(', ')}` });
|
||||||
|
}
|
||||||
|
const dur = Math.max(0, parseInt(duration_s, 10) || 0);
|
||||||
|
const ev = await pool.query(
|
||||||
|
`SELECT COALESCE(MAX(event_id), 0) + 1 AS next FROM playout_scte_breaks WHERE channel_id = $1`,
|
||||||
|
[req.channel.id]);
|
||||||
|
const eventId = ev.rows[0].next;
|
||||||
|
const { rows } = await pool.query(
|
||||||
|
`INSERT INTO playout_scte_breaks (channel_id, duration_s, event_id, type, status, created_by)
|
||||||
|
VALUES ($1,$2,$3,$4,'pending',$5) RETURNING *`,
|
||||||
|
[req.channel.id, dur, eventId, type, req.user?.id || null]);
|
||||||
|
try {
|
||||||
|
const out = await fireScteBreak(req.channel, rows[0]);
|
||||||
|
const updated = await pool.query('SELECT * FROM playout_scte_breaks WHERE id = $1', [rows[0].id]);
|
||||||
|
res.json({ break: updated.rows[0], engine: out });
|
||||||
|
} catch (err) {
|
||||||
|
await pool.query(`UPDATE playout_scte_breaks SET status = 'cancelled', updated_at = NOW() WHERE id = $1`,
|
||||||
|
[rows[0].id]).catch(() => {});
|
||||||
|
return res.status(502).json({ error: 'Sidecar unreachable: ' + err.message });
|
||||||
|
}
|
||||||
|
} catch (err) { next(err); }
|
||||||
|
});
|
||||||
|
|
||||||
|
router.delete('/channels/:id/scte/:scteId', requireChannelEdit, async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { rows } = await pool.query(
|
||||||
|
`UPDATE playout_scte_breaks SET status = 'cancelled', updated_at = NOW()
|
||||||
|
WHERE id = $1 AND channel_id = $2 AND status = 'pending' RETURNING id`,
|
||||||
|
[req.params.scteId, req.channel.id]);
|
||||||
|
if (rows.length === 0) return res.status(404).json({ error: 'Pending break not found' });
|
||||||
|
res.json({ cancelled: true });
|
||||||
|
} catch (err) { next(err); }
|
||||||
|
});
|
||||||
|
|
||||||
async function loadChannelForBody(req, res, next) {
|
async function loadChannelForBody(req, res, next) {
|
||||||
// For playlist/item routes the channel is referenced indirectly; resolve it
|
|
||||||
// and assert edit. Used on create/mutate routes that carry channel_id.
|
|
||||||
const channelId = req.body.channel_id || req.query.channel_id;
|
const channelId = req.body.channel_id || req.query.channel_id;
|
||||||
if (!channelId) return res.status(400).json({ error: 'channel_id is required' });
|
if (!channelId) return res.status(400).json({ error: 'channel_id is required' });
|
||||||
try {
|
try {
|
||||||
|
|
@ -528,7 +599,6 @@ async function loadChannelForBody(req, res, next) {
|
||||||
} catch (err) { next(err); }
|
} catch (err) { next(err); }
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /playout/playlists?channel_id=...
|
|
||||||
router.get('/playlists', async (req, res, next) => {
|
router.get('/playlists', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const channelId = req.query.channel_id;
|
const channelId = req.query.channel_id;
|
||||||
|
|
@ -542,7 +612,6 @@ router.get('/playlists', async (req, res, next) => {
|
||||||
} catch (err) { next(err); }
|
} catch (err) { next(err); }
|
||||||
});
|
});
|
||||||
|
|
||||||
// POST /playout/playlists
|
|
||||||
router.post('/playlists', loadChannelForBody, async (req, res, next) => {
|
router.post('/playlists', loadChannelForBody, async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { name, loop = false } = req.body || {};
|
const { name, loop = false } = req.body || {};
|
||||||
|
|
@ -554,7 +623,6 @@ router.post('/playlists', loadChannelForBody, async (req, res, next) => {
|
||||||
} catch (err) { next(err); }
|
} catch (err) { next(err); }
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /playout/playlists/:plid/items
|
|
||||||
router.get('/playlists/:plid/items', validateUuid('plid'), async (req, res, next) => {
|
router.get('/playlists/:plid/items', validateUuid('plid'), async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const pl = await pool.query(
|
const pl = await pool.query(
|
||||||
|
|
@ -570,7 +638,6 @@ router.get('/playlists/:plid/items', validateUuid('plid'), async (req, res, next
|
||||||
} catch (err) { next(err); }
|
} catch (err) { next(err); }
|
||||||
});
|
});
|
||||||
|
|
||||||
// Helper: load a playlist + assert edit on its channel's project.
|
|
||||||
async function loadPlaylistEdit(plid, user) {
|
async function loadPlaylistEdit(plid, user) {
|
||||||
const pl = await pool.query(
|
const pl = await pool.query(
|
||||||
`SELECT p.*, c.project_id FROM playout_playlists p
|
`SELECT p.*, c.project_id FROM playout_playlists p
|
||||||
|
|
@ -580,7 +647,6 @@ async function loadPlaylistEdit(plid, user) {
|
||||||
return pl.rows[0];
|
return pl.rows[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /playout/playlists/:plid/items — add an asset to a playlist
|
|
||||||
router.post('/playlists/:plid/items', validateUuid('plid'), async (req, res, next) => {
|
router.post('/playlists/:plid/items', validateUuid('plid'), async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
await loadPlaylistEdit(req.params.plid, req.user);
|
await loadPlaylistEdit(req.params.plid, req.user);
|
||||||
|
|
@ -588,7 +654,6 @@ router.post('/playlists/:plid/items', validateUuid('plid'), async (req, res, nex
|
||||||
transition = 'cut', transition_ms = 0 } = req.body || {};
|
transition = 'cut', transition_ms = 0 } = req.body || {};
|
||||||
if (!asset_id) return res.status(400).json({ error: 'asset_id is required' });
|
if (!asset_id) return res.status(400).json({ error: 'asset_id is required' });
|
||||||
|
|
||||||
// Append at the end of the playlist.
|
|
||||||
const ord = await pool.query(
|
const ord = await pool.query(
|
||||||
'SELECT COALESCE(MAX(sort_order), -1) + 1 AS next FROM playout_items WHERE playlist_id = $1',
|
'SELECT COALESCE(MAX(sort_order), -1) + 1 AS next FROM playout_items WHERE playlist_id = $1',
|
||||||
[req.params.plid]);
|
[req.params.plid]);
|
||||||
|
|
@ -597,8 +662,6 @@ router.post('/playlists/:plid/items', validateUuid('plid'), async (req, res, nex
|
||||||
VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING *`,
|
VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING *`,
|
||||||
[req.params.plid, asset_id, ord.rows[0].next, in_point, out_point, transition, transition_ms]);
|
[req.params.plid, asset_id, ord.rows[0].next, in_point, out_point, transition, transition_ms]);
|
||||||
|
|
||||||
// Kick staging immediately so the clip is air-ready by the time the operator
|
|
||||||
// hits play.
|
|
||||||
await stageQueue.add('stage', { itemId: rows[0].id, assetId: asset_id }).catch((e) =>
|
await stageQueue.add('stage', { itemId: rows[0].id, assetId: asset_id }).catch((e) =>
|
||||||
console.error('[playout] failed to enqueue stage job:', e.message));
|
console.error('[playout] failed to enqueue stage job:', e.message));
|
||||||
|
|
||||||
|
|
@ -609,7 +672,6 @@ router.post('/playlists/:plid/items', validateUuid('plid'), async (req, res, nex
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// PUT /playout/playlists/:plid/reorder — body { order: [itemId, itemId, ...] }
|
|
||||||
router.put('/playlists/:plid/reorder', validateUuid('plid'), async (req, res, next) => {
|
router.put('/playlists/:plid/reorder', validateUuid('plid'), async (req, res, next) => {
|
||||||
const client = await pool.connect();
|
const client = await pool.connect();
|
||||||
try {
|
try {
|
||||||
|
|
@ -631,7 +693,6 @@ router.put('/playlists/:plid/reorder', validateUuid('plid'), async (req, res, ne
|
||||||
} finally { client.release(); }
|
} finally { client.release(); }
|
||||||
});
|
});
|
||||||
|
|
||||||
// DELETE /playout/items/:itemId
|
|
||||||
router.delete('/items/:itemId', validateUuid('itemId'), async (req, res, next) => {
|
router.delete('/items/:itemId', validateUuid('itemId'), async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const it = await pool.query(
|
const it = await pool.query(
|
||||||
|
|
@ -645,7 +706,6 @@ router.delete('/items/:itemId', validateUuid('itemId'), async (req, res, next) =
|
||||||
} catch (err) { next(err); }
|
} catch (err) { next(err); }
|
||||||
});
|
});
|
||||||
|
|
||||||
// POST /playout/items/:itemId/stage — (re)kick staging for one item
|
|
||||||
router.post('/items/:itemId/stage', validateUuid('itemId'), async (req, res, next) => {
|
router.post('/items/:itemId/stage', validateUuid('itemId'), async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const it = await pool.query(
|
const it = await pool.query(
|
||||||
|
|
@ -660,13 +720,6 @@ router.post('/items/:itemId/stage', validateUuid('itemId'), async (req, res, nex
|
||||||
} catch (err) { next(err); }
|
} catch (err) { next(err); }
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Failover (called by scheduler tick) ──────────────────────────────────────
|
|
||||||
// Tear down a (presumed dead) sidecar and re-spawn it on another cluster node
|
|
||||||
// matching the original capability. DeckLink channels are excluded — the
|
|
||||||
// device-index pinning makes blind re-placement risky, so they alert only.
|
|
||||||
//
|
|
||||||
// Returns { restarted: true, new_node_id } on success, or { restarted: false,
|
|
||||||
// reason } when no eligible node exists or the channel is decklink.
|
|
||||||
export async function restartChannel(channelId) {
|
export async function restartChannel(channelId) {
|
||||||
const { rows } = await pool.query('SELECT * FROM playout_channels WHERE id = $1', [channelId]);
|
const { rows } = await pool.query('SELECT * FROM playout_channels WHERE id = $1', [channelId]);
|
||||||
if (rows.length === 0) return { restarted: false, reason: 'channel not found' };
|
if (rows.length === 0) return { restarted: false, reason: 'channel not found' };
|
||||||
|
|
@ -676,7 +729,6 @@ export async function restartChannel(channelId) {
|
||||||
return { restarted: false, reason: 'decklink channels are alert-only' };
|
return { restarted: false, reason: 'decklink channels are alert-only' };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Best-effort teardown of the old container — it may already be dead.
|
|
||||||
if (channel.container_id) {
|
if (channel.container_id) {
|
||||||
const { remote, apiUrl } = await resolveNodeTarget(channel.node_id);
|
const { remote, apiUrl } = await resolveNodeTarget(channel.node_id);
|
||||||
if (remote && apiUrl) {
|
if (remote && apiUrl) {
|
||||||
|
|
@ -690,9 +742,6 @@ export async function restartChannel(channelId) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pick a different healthy node. For NDI/SRT/RTMP every online node is
|
|
||||||
// eligible (no hardware contention). Prefer the original if it's still
|
|
||||||
// online — the failure may have been transient.
|
|
||||||
const nodes = await pool.query(
|
const nodes = await pool.query(
|
||||||
`SELECT id, hostname, api_url, last_seen_at FROM cluster_nodes
|
`SELECT id, hostname, api_url, last_seen_at FROM cluster_nodes
|
||||||
WHERE id <> $1 AND last_seen_at > NOW() - INTERVAL '60 seconds'
|
WHERE id <> $1 AND last_seen_at > NOW() - INTERVAL '60 seconds'
|
||||||
|
|
@ -708,9 +757,6 @@ export async function restartChannel(channelId) {
|
||||||
}
|
}
|
||||||
const newNodeId = nodes.rows[0].id;
|
const newNodeId = nodes.rows[0].id;
|
||||||
|
|
||||||
// Move the channel to the new node + bump the restart counters; the operator
|
|
||||||
// UI surfaces these to flag restarts. container_meta is cleared so the new
|
|
||||||
// spawn re-derives the sidecar URL.
|
|
||||||
const { rows: moved } = await pool.query(
|
const { rows: moved } = await pool.query(
|
||||||
`UPDATE playout_channels
|
`UPDATE playout_channels
|
||||||
SET node_id = $1, status = 'stopped', container_id = NULL, container_meta = '{}'::jsonb,
|
SET node_id = $1, status = 'stopped', container_id = NULL, container_meta = '{}'::jsonb,
|
||||||
|
|
@ -720,10 +766,6 @@ export async function restartChannel(channelId) {
|
||||||
[newNodeId, channel.id]
|
[newNodeId, channel.id]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Spawn the sidecar directly via the shared helper. We do NOT route through
|
|
||||||
// the HTTP /start endpoint: its guard rejects status 'starting'/'running' and
|
|
||||||
// would deadlock the failover. spawnChannelSidecar flips the channel to
|
|
||||||
// running (or leaves it 'error' and throws on spawn failure).
|
|
||||||
try {
|
try {
|
||||||
await spawnChannelSidecar(moved[0]);
|
await spawnChannelSidecar(moved[0]);
|
||||||
return { restarted: true, new_node_id: newNodeId };
|
return { restarted: true, new_node_id: newNodeId };
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,16 @@
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import http from 'http';
|
import http from 'http';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { createReadStream, existsSync } from 'fs';
|
|
||||||
import { stat } from 'fs/promises';
|
|
||||||
import net from 'net';
|
import net from 'net';
|
||||||
import dgram from 'dgram';
|
import dgram from 'dgram';
|
||||||
import pool from '../db/pool.js';
|
import pool from '../db/pool.js';
|
||||||
import { s3Client, getS3Bucket } from '../s3/client.js';
|
import { getS3Bucket } from '../s3/client.js';
|
||||||
import { Upload } from '@aws-sdk/lib-storage';
|
|
||||||
import { validateUuid } from '../middleware/errors.js';
|
import { validateUuid } from '../middleware/errors.js';
|
||||||
import { assertProjectAccess, accessibleProjectIds } from '../auth/authz.js';
|
import { assertProjectAccess, accessibleProjectIds } from '../auth/authz.js';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { Queue } from 'bullmq';
|
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// BullMQ proxy queue — used by the growing-file stop handler to queue proxy
|
|
||||||
// jobs when the capture container's finalize call races with the S3 upload.
|
|
||||||
const parseRedisUrl = (url) => {
|
|
||||||
const parsed = new URL(url);
|
|
||||||
return { host: parsed.hostname, port: parseInt(parsed.port, 10) || 6379 };
|
|
||||||
};
|
|
||||||
const proxyQueue = new Queue('proxy', {
|
|
||||||
connection: parseRedisUrl(process.env.REDIS_URL || 'redis://queue:6379'),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Every /:id recorder route is scoped to the recorder's project. The param
|
// Every /:id recorder route is scoped to the recorder's project. The param
|
||||||
// handler validates the UUID, resolves the owning project_id, and asserts the
|
// handler validates the UUID, resolves the owning project_id, and asserts the
|
||||||
// 'view' baseline; mutating routes escalate to 'edit' via requireRecorderEdit.
|
// 'view' baseline; mutating routes escalate to 'edit' via requireRecorderEdit.
|
||||||
|
|
@ -179,6 +165,94 @@ function pickRecorderFields(body) {
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Codecs that require an NVIDIA GPU on the target node.
|
||||||
|
const GPU_CODECS = ['hevc_nvenc', 'h264_nvenc'];
|
||||||
|
|
||||||
|
// Issue #163 — codec/container/audio compatibility guard. Returns null when the
|
||||||
|
// config is valid, otherwise a descriptive error string naming the bad combo.
|
||||||
|
// `nodeHasGpu` is tri-state: true (GPU present), false (no GPU), or null
|
||||||
|
// (unknown — node not resolvable at this point, so GPU is only a soft check).
|
||||||
|
//
|
||||||
|
// Rules:
|
||||||
|
// - PCM audio is only valid in MOV/MXF containers, never MP4 (an MP4 with a
|
||||||
|
// PCM track produces a corrupt/unplayable master — also part of #162).
|
||||||
|
// - HEVC is not valid in MXF in this build.
|
||||||
|
// - NVENC codecs require the target node to have a GPU.
|
||||||
|
function validateRecorderConfig(cfg, nodeHasGpu = null) {
|
||||||
|
if (!cfg) return null;
|
||||||
|
|
||||||
|
const container = String(cfg.recording_container || '').toLowerCase();
|
||||||
|
const codec = String(cfg.recording_codec || '').toLowerCase();
|
||||||
|
const audio = String(cfg.recording_audio_codec || '').toLowerCase();
|
||||||
|
|
||||||
|
// PCM audio + MP4 → reject.
|
||||||
|
if (container === 'mp4' && audio.startsWith('pcm')) {
|
||||||
|
return `Invalid combo: PCM audio (${cfg.recording_audio_codec}) is not supported in an MP4 container. Use a MOV or MXF container, or switch the audio codec to AAC.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// HEVC in MXF → reject.
|
||||||
|
if (container === 'mxf' && (codec === 'hevc' || codec === 'hevc_nvenc')) {
|
||||||
|
return `Invalid combo: HEVC (${cfg.recording_codec}) is not supported in an MXF container in this build. Use a MOV/MP4 container, or pick a DNxHR/ProRes codec for MXF.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NVENC requires a GPU on the target node. Only a hard error when we know the
|
||||||
|
// node lacks one; unknown capability is left as a soft pass.
|
||||||
|
if (GPU_CODECS.includes(codec) && nodeHasGpu === false) {
|
||||||
|
return `Invalid combo: codec ${cfg.recording_codec} requires an NVIDIA GPU, but the target node reports no GPU. Choose a software codec (e.g. prores_hq, dnxhr_hq, h264) or assign a GPU node.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve whether a recorder's target node has a GPU. Returns true/false when
|
||||||
|
// the node's heartbeat capability is known, or null when it can't be resolved
|
||||||
|
// (no node assigned / no capability reported) — callers treat null as a soft
|
||||||
|
// check per validateRecorderConfig.
|
||||||
|
async function nodeHasGpuCapability(nodeId) {
|
||||||
|
if (!nodeId) return null;
|
||||||
|
try {
|
||||||
|
const r = await pool.query(
|
||||||
|
'SELECT capabilities FROM cluster_nodes WHERE id = $1',
|
||||||
|
[nodeId]
|
||||||
|
);
|
||||||
|
if (r.rows.length === 0) return null;
|
||||||
|
const caps = r.rows[0].capabilities;
|
||||||
|
const gpus = caps && caps.gpus;
|
||||||
|
if (!Array.isArray(gpus)) return null;
|
||||||
|
return gpus.length > 0;
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
||||||
|
|
||||||
|
// Issue #162 — after a local-spawn stop, wait for the capture container to
|
||||||
|
// finalize its master. The asset row was pre-created at start with
|
||||||
|
// status='live' (display_name = current_session_id); the ingest/finalize step
|
||||||
|
// flips it to ready/processing once the MOV/MP4 trailer is written. We poll
|
||||||
|
// until the asset leaves 'live' (or disappears) or we hit the timeout, so we
|
||||||
|
// don't DELETE the container — and SIGKILL ffmpeg — before the trailer lands.
|
||||||
|
async function waitForFinalize(recorder, { timeoutMs = 180000, intervalMs = 3000 } = {}) {
|
||||||
|
if (!recorder.current_session_id) return;
|
||||||
|
const deadline = Date.now() + timeoutMs;
|
||||||
|
while (Date.now() < deadline) {
|
||||||
|
try {
|
||||||
|
const r = await pool.query(
|
||||||
|
`SELECT 1 FROM assets
|
||||||
|
WHERE project_id = $1
|
||||||
|
AND display_name = $2
|
||||||
|
AND status = 'live'
|
||||||
|
LIMIT 1`,
|
||||||
|
[recorder.project_id, recorder.current_session_id]
|
||||||
|
);
|
||||||
|
// No live asset row left → finalize is done (or there was none to wait on).
|
||||||
|
if (r.rows.length === 0) return;
|
||||||
|
} catch (_) { /* transient DB error — keep polling until timeout */ }
|
||||||
|
await sleep(intervalMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// GET / - List all recorders
|
// GET / - List all recorders
|
||||||
//
|
//
|
||||||
// Issue #121 — previous version fired N PG queries + N Docker inspects per
|
// Issue #121 — previous version fired N PG queries + N Docker inspects per
|
||||||
|
|
@ -269,6 +343,13 @@ router.post('/', async (req, res, next) => {
|
||||||
};
|
};
|
||||||
const row = { id: uuidv4(), status: 'stopped', ...defaults, ...fields };
|
const row = { id: uuidv4(), status: 'stopped', ...defaults, ...fields };
|
||||||
|
|
||||||
|
// Issue #163 — reject invalid codec/container/audio combos before insert.
|
||||||
|
const createGpu = await nodeHasGpuCapability(row.node_id);
|
||||||
|
const createErr = validateRecorderConfig(row, createGpu);
|
||||||
|
if (createErr) {
|
||||||
|
return res.status(400).json({ error: createErr });
|
||||||
|
}
|
||||||
|
|
||||||
// Build INSERT dynamically so adding columns later means one place to update.
|
// Build INSERT dynamically so adding columns later means one place to update.
|
||||||
const cols = Object.keys(row);
|
const cols = Object.keys(row);
|
||||||
const placeholders = cols.map((_, i) => `$${i + 1}`).join(', ');
|
const placeholders = cols.map((_, i) => `$${i + 1}`).join(', ');
|
||||||
|
|
@ -335,6 +416,15 @@ router.patch('/:id', requireRecorderEdit, async (req, res, next) => {
|
||||||
return res.status(400).json({ error: 'No fields to update' });
|
return res.status(400).json({ error: 'No fields to update' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Issue #163 — validate the resulting config (existing row overlaid with the
|
||||||
|
// incoming changes) so a PATCH can't introduce an invalid combo either.
|
||||||
|
const merged = { ...recorder, ...fields };
|
||||||
|
const patchGpu = await nodeHasGpuCapability(merged.node_id);
|
||||||
|
const patchErr = validateRecorderConfig(merged, patchGpu);
|
||||||
|
if (patchErr) {
|
||||||
|
return res.status(400).json({ error: patchErr });
|
||||||
|
}
|
||||||
|
|
||||||
const setClause = cols.map((k, i) => `${k} = $${i + 1}`).join(', ');
|
const setClause = cols.map((k, i) => `${k} = $${i + 1}`).join(', ');
|
||||||
const params = cols.map(k => fields[k]);
|
const params = cols.map(k => fields[k]);
|
||||||
params.push(id);
|
params.push(id);
|
||||||
|
|
@ -510,11 +600,15 @@ router.post('/:id/start', requireRecorderEdit, async (req, res, next) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GPU-accelerated codecs require the NVIDIA container runtime on the node.
|
// GPU-accelerated codecs require the NVIDIA container runtime on the node.
|
||||||
// hevc_nvenc / h264_nvenc are the only two we currently support; extend
|
// hevc_nvenc / h264_nvenc are the only two we currently support (see the
|
||||||
// this list if av1_nvenc or others are added later.
|
// module-level GPU_CODECS list); extend it if av1_nvenc or others are added.
|
||||||
const GPU_CODECS = ['hevc_nvenc', 'h264_nvenc'];
|
|
||||||
const useGpu = GPU_CODECS.includes(recorder.recording_codec);
|
const useGpu = GPU_CODECS.includes(recorder.recording_codec);
|
||||||
|
|
||||||
|
// Issue #167 — per-recorder GPU affinity. When recorders.gpu_uuid is set the
|
||||||
|
// sidecar is pinned to that single device (NVIDIA_VISIBLE_DEVICES=<uuid>);
|
||||||
|
// null keeps the legacy "all" behavior. Only meaningful when useGpu is true.
|
||||||
|
const gpuUuid = recorder.gpu_uuid || null;
|
||||||
|
|
||||||
// Determine whether to spawn locally or via a remote node-agent.
|
// Determine whether to spawn locally or via a remote node-agent.
|
||||||
const { remote: isRemote, apiUrl: targetNodeApiUrl } = await resolveNodeTarget(recorder.node_id);
|
const { remote: isRemote, apiUrl: targetNodeApiUrl } = await resolveNodeTarget(recorder.node_id);
|
||||||
// For remote sidecars, the capture container runs on the worker host network and cannot
|
// For remote sidecars, the capture container runs on the worker host network and cannot
|
||||||
|
|
@ -532,7 +626,7 @@ router.post('/:id/start', requireRecorderEdit, async (req, res, next) => {
|
||||||
const sidecarRes = await fetch(`${targetNodeApiUrl}/sidecar/start`, {
|
const sidecarRes = await fetch(`${targetNodeApiUrl}/sidecar/start`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ image: 'wild-dragon-capture:latest', env, capturePort, sourceType, useGpu }),
|
body: JSON.stringify({ image: 'wild-dragon-capture:latest', env, capturePort, sourceType, useGpu, gpuUuid }),
|
||||||
signal: AbortSignal.timeout(15000),
|
signal: AbortSignal.timeout(15000),
|
||||||
});
|
});
|
||||||
if (!sidecarRes.ok) {
|
if (!sidecarRes.ok) {
|
||||||
|
|
@ -575,7 +669,8 @@ router.post('/:id/start', requireRecorderEdit, async (req, res, next) => {
|
||||||
|
|
||||||
const localEnv = [...env];
|
const localEnv = [...env];
|
||||||
if (useGpu) {
|
if (useGpu) {
|
||||||
localEnv.push('NVIDIA_VISIBLE_DEVICES=all');
|
// Issue #167 — same per-recorder GPU affinity as the remote sidecar path.
|
||||||
|
localEnv.push(`NVIDIA_VISIBLE_DEVICES=${gpuUuid || 'all'}`);
|
||||||
localEnv.push('NVIDIA_DRIVER_CAPABILITIES=video,compute,utility');
|
localEnv.push('NVIDIA_DRIVER_CAPABILITIES=video,compute,utility');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -677,9 +772,13 @@ router.post('/:id/stop', requireRecorderEdit, async (req, res, next) => {
|
||||||
return res.status(502).json({ error: 'Remote node failed to stop sidecar' });
|
return res.status(502).json({ error: 'Remote node failed to stop sidecar' });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Issue #162 — stop WITH a grace period (t=180). Docker sends SIGTERM and
|
||||||
|
// waits up to 180s for ffmpeg to flush and write the MOV/MP4 trailer before
|
||||||
|
// it SIGKILLs. Without this the master is truncated/corrupt and the
|
||||||
|
// pre-created asset can get stuck in 'live'.
|
||||||
const stopRes = await dockerApi(
|
const stopRes = await dockerApi(
|
||||||
'POST',
|
'POST',
|
||||||
`/containers/${recorder.container_id}/stop`
|
`/containers/${recorder.container_id}/stop?t=180`
|
||||||
);
|
);
|
||||||
|
|
||||||
// 204 = stopped, 304 = already stopped, 404 = container gone — all acceptable.
|
// 204 = stopped, 304 = already stopped, 404 = container gone — all acceptable.
|
||||||
|
|
@ -692,6 +791,12 @@ router.post('/:id/stop', requireRecorderEdit, async (req, res, next) => {
|
||||||
|
|
||||||
// Only attempt remove if the container existed (not 404).
|
// Only attempt remove if the container existed (not 404).
|
||||||
if (stopRes.status !== 404) {
|
if (stopRes.status !== 404) {
|
||||||
|
// Issue #162 — before removing the container, wait for the master to
|
||||||
|
// finalize (asset leaves 'live'), mirroring the remote path's reliance on
|
||||||
|
// the node-agent's clean teardown. This guards against deleting the
|
||||||
|
// container — and its lingering finalize work — too early.
|
||||||
|
await waitForFinalize(recorder);
|
||||||
|
|
||||||
const removeRes = await dockerApi(
|
const removeRes = await dockerApi(
|
||||||
'DELETE',
|
'DELETE',
|
||||||
`/containers/${recorder.container_id}`
|
`/containers/${recorder.container_id}`
|
||||||
|
|
@ -706,28 +811,6 @@ router.post('/:id/stop', requireRecorderEdit, async (req, res, next) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Growing-files S3 promotion ────────────────────────────────────────────
|
|
||||||
// When growing_enabled=true the capture container writes the master file to
|
|
||||||
// /growing/{projectId}/{clipName}.{ext} (a host bind-mount that the mam-api
|
|
||||||
// container also has at /growing). The capture container's graceful-shutdown
|
|
||||||
// handler (triggered by the Docker stop above) calls POST /assets/:id/finalize
|
|
||||||
// with the expected S3 key, which queues the proxy job — but the file was
|
|
||||||
// never uploaded to S3, so the proxy worker fails with "unable to open file".
|
|
||||||
//
|
|
||||||
// Fix: after the container has exited (ffmpeg is done flushing), upload the
|
|
||||||
// growing file to the canonical S3 key from here. This is synchronous and
|
|
||||||
// completes before the HTTP response reaches the client, so the already-queued
|
|
||||||
// proxy job will find a valid S3 object when the worker dequeues it.
|
|
||||||
//
|
|
||||||
// Only applies to LOCAL recorders — remote recorders write to a different
|
|
||||||
// node's /growing mount which this process cannot access.
|
|
||||||
if (!isRemote && recorder.growing_enabled === true && recorder.current_session_id) {
|
|
||||||
await promoteGrowingFileToS3(recorder).catch(err => {
|
|
||||||
// Non-fatal — log and continue so the stop always succeeds.
|
|
||||||
console.error('[recorders/stop] growing-file promotion failed (non-fatal):', err.message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateResult = await pool.query(
|
const updateResult = await pool.query(
|
||||||
`UPDATE recorders
|
`UPDATE recorders
|
||||||
SET container_id = NULL, status = $1, updated_at = NOW()
|
SET container_id = NULL, status = $1, updated_at = NOW()
|
||||||
|
|
@ -742,109 +825,6 @@ router.post('/:id/stop', requireRecorderEdit, async (req, res, next) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Upload a completed growing-file master from /growing to S3 so the proxy
|
|
||||||
* worker can find it at the expected original_s3_key.
|
|
||||||
*
|
|
||||||
* The capture container writes to:
|
|
||||||
* /growing/{projectId}/{clipName}.{ext}
|
|
||||||
*
|
|
||||||
* The canonical S3 key (set on the asset row at recording start) is:
|
|
||||||
* projects/{projectId}/masters/{clipName}.{ext}
|
|
||||||
*
|
|
||||||
* We look up the live/processing asset to derive both paths, do a multipart
|
|
||||||
* upload, update the asset's original_s3_key and file_size to match what we
|
|
||||||
* actually uploaded, then ensure a proxy job exists for it.
|
|
||||||
*/
|
|
||||||
async function promoteGrowingFileToS3(recorder) {
|
|
||||||
const clipName = recorder.current_session_id;
|
|
||||||
const container = recorder.recording_container || 'mov';
|
|
||||||
|
|
||||||
// Find the asset that was pre-created at recording start. It could be in
|
|
||||||
// 'live' (finalize hasn't fired yet) or 'processing' (finalize already ran
|
|
||||||
// from the container's SIGTERM handler). We need both its id and its
|
|
||||||
// project_id to reconstruct the growing path.
|
|
||||||
const assetRes = await pool.query(
|
|
||||||
`SELECT id, project_id, status, original_s3_key
|
|
||||||
FROM assets
|
|
||||||
WHERE display_name = $1
|
|
||||||
AND status IN ('live', 'processing', 'error')
|
|
||||||
ORDER BY created_at DESC
|
|
||||||
LIMIT 1`,
|
|
||||||
[clipName]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (assetRes.rows.length === 0) {
|
|
||||||
console.warn(`[recorders/stop] no asset found for clip "${clipName}" — skipping growing-file promotion`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const asset = assetRes.rows[0];
|
|
||||||
const projectId = asset.project_id;
|
|
||||||
const growingDir = process.env.GROWING_DIR || '/growing';
|
|
||||||
const localPath = `${growingDir}/${projectId}/${clipName}.${container}`;
|
|
||||||
const s3Key = `projects/${projectId}/masters/${clipName}.${container}`;
|
|
||||||
|
|
||||||
if (!existsSync(localPath)) {
|
|
||||||
console.warn(`[recorders/stop] growing file not found at ${localPath} — nothing to promote (empty recording?)`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileStat = await stat(localPath);
|
|
||||||
if (fileStat.size === 0) {
|
|
||||||
console.warn(`[recorders/stop] growing file at ${localPath} is empty — skipping promotion`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[recorders/stop] promoting growing file ${localPath} (${fileStat.size} bytes) → s3://${getS3Bucket()}/${s3Key}`);
|
|
||||||
|
|
||||||
const upload = new Upload({
|
|
||||||
client: s3Client,
|
|
||||||
params: {
|
|
||||||
Bucket: getS3Bucket(),
|
|
||||||
Key: s3Key,
|
|
||||||
Body: createReadStream(localPath),
|
|
||||||
},
|
|
||||||
queueSize: 4,
|
|
||||||
partSize: 8 * 1024 * 1024,
|
|
||||||
});
|
|
||||||
await upload.done();
|
|
||||||
|
|
||||||
console.log(`[recorders/stop] S3 upload complete for ${s3Key}`);
|
|
||||||
|
|
||||||
// Ensure the asset row reflects the correct S3 key and file size. The
|
|
||||||
// capture container's finalize call may have already set original_s3_key to
|
|
||||||
// this same value (it was pre-set at start), but update file_size which
|
|
||||||
// finalize doesn't touch.
|
|
||||||
await pool.query(
|
|
||||||
`UPDATE assets
|
|
||||||
SET original_s3_key = $1,
|
|
||||||
file_size = $2,
|
|
||||||
updated_at = NOW()
|
|
||||||
WHERE id = $3`,
|
|
||||||
[s3Key, fileStat.size, asset.id]
|
|
||||||
);
|
|
||||||
|
|
||||||
// If the asset is still 'live' (capture container's finalize hasn't fired or
|
|
||||||
// failed), flip it to 'processing' and queue the proxy job ourselves so the
|
|
||||||
// clip doesn't get stuck in the library as "Recording…".
|
|
||||||
if (asset.status === 'live') {
|
|
||||||
console.log(`[recorders/stop] finalize not yet called — queueing proxy and flipping asset ${asset.id} to processing`);
|
|
||||||
await pool.query(
|
|
||||||
`UPDATE assets SET status = 'processing', updated_at = NOW() WHERE id = $1`,
|
|
||||||
[asset.id]
|
|
||||||
);
|
|
||||||
await proxyQueue.add('generate', {
|
|
||||||
assetId: asset.id,
|
|
||||||
inputKey: s3Key,
|
|
||||||
outputKey: `proxies/${asset.id}.mp4`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// If status is already 'processing', the capture container's finalize already
|
|
||||||
// ran and queued the proxy job. The S3 upload we just did ensures the worker
|
|
||||||
// will find a valid object when it dequeues that job — nothing else to do.
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /:id/status - Get live status
|
// GET /:id/status - Get live status
|
||||||
router.get('/:id/status', async (req, res, next) => {
|
router.get('/:id/status', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,12 @@
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import pool from '../db/pool.js';
|
import pool from '../db/pool.js';
|
||||||
import { hashPassword } from '../auth/passwords.js';
|
import { hashPassword } from '../auth/passwords.js';
|
||||||
import { DEV_USER_ID } from '../middleware/auth.js';
|
import { DEV_USER_ID, requireAdmin } from '../middleware/auth.js';
|
||||||
|
import { accessibleProjectIds } from '../auth/authz.js';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const MIN_PASSWORD_LEN = 12;
|
const MIN_PASSWORD_LEN = 12;
|
||||||
|
const ROLES = ['admin', 'editor', 'viewer'];
|
||||||
|
|
||||||
function bad(res, msg) { return res.status(400).json({ error: msg }); }
|
function bad(res, msg) { return res.status(400).json({ error: msg }); }
|
||||||
|
|
||||||
|
|
@ -14,7 +16,7 @@ function bad(res, msg) { return res.status(400).json({ error: msg }); }
|
||||||
router.get('/', async (_req, res, next) => {
|
router.get('/', async (_req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { rows } = await pool.query(
|
const { rows } = await pool.query(
|
||||||
`SELECT id, username, display_name, role, last_login_at, created_at
|
`SELECT id, username, display_name, role, totp_enabled, last_login_at, created_at
|
||||||
FROM users WHERE id <> $1 ORDER BY username`, [DEV_USER_ID]);
|
FROM users WHERE id <> $1 ORDER BY username`, [DEV_USER_ID]);
|
||||||
res.json(rows);
|
res.json(rows);
|
||||||
} catch (err) { next(err); }
|
} catch (err) { next(err); }
|
||||||
|
|
@ -26,6 +28,7 @@ router.post('/', async (req, res, next) => {
|
||||||
const { username, password, display_name, role } = req.body || {};
|
const { username, password, display_name, role } = req.body || {};
|
||||||
if (!username || typeof username !== 'string') return bad(res, 'username required');
|
if (!username || typeof username !== 'string') return bad(res, 'username required');
|
||||||
if (!password || password.length < MIN_PASSWORD_LEN) return bad(res, 'password must be at least ' + MIN_PASSWORD_LEN + ' chars');
|
if (!password || password.length < MIN_PASSWORD_LEN) return bad(res, 'password must be at least ' + MIN_PASSWORD_LEN + ' chars');
|
||||||
|
if (role !== undefined && !ROLES.includes(role)) return bad(res, "role must be one of: " + ROLES.join(', '));
|
||||||
const hash = await hashPassword(password);
|
const hash = await hashPassword(password);
|
||||||
const { rows } = await pool.query(
|
const { rows } = await pool.query(
|
||||||
`INSERT INTO users (username, password_hash, display_name, role)
|
`INSERT INTO users (username, password_hash, display_name, role)
|
||||||
|
|
@ -76,7 +79,10 @@ router.patch('/:id', async (req, res, next) => {
|
||||||
if (req.params.id === DEV_USER_ID) return res.status(400).json({ error: 'cannot edit dev user' });
|
if (req.params.id === DEV_USER_ID) return res.status(400).json({ error: 'cannot edit dev user' });
|
||||||
const sets = []; const vals = [];
|
const sets = []; const vals = [];
|
||||||
if (typeof req.body?.display_name === 'string') { sets.push('display_name = $' + (sets.length + 1)); vals.push(req.body.display_name); }
|
if (typeof req.body?.display_name === 'string') { sets.push('display_name = $' + (sets.length + 1)); vals.push(req.body.display_name); }
|
||||||
if (typeof req.body?.role === 'string') { sets.push('role = $' + (sets.length + 1)); vals.push(req.body.role); }
|
if (typeof req.body?.role === 'string') {
|
||||||
|
if (!ROLES.includes(req.body.role)) return bad(res, "role must be one of: " + ROLES.join(', '));
|
||||||
|
sets.push('role = $' + (sets.length + 1)); vals.push(req.body.role);
|
||||||
|
}
|
||||||
if (typeof req.body?.password === 'string') {
|
if (typeof req.body?.password === 'string') {
|
||||||
if (req.body.password.length < MIN_PASSWORD_LEN) return bad(res, 'password must be at least ' + MIN_PASSWORD_LEN + ' chars');
|
if (req.body.password.length < MIN_PASSWORD_LEN) return bad(res, 'password must be at least ' + MIN_PASSWORD_LEN + ' chars');
|
||||||
sets.push('password_hash = $' + (sets.length + 1) + ', password_updated_at = NOW()');
|
sets.push('password_hash = $' + (sets.length + 1) + ', password_updated_at = NOW()');
|
||||||
|
|
@ -93,4 +99,88 @@ router.patch('/:id', async (req, res, next) => {
|
||||||
} catch (err) { next(err); }
|
} catch (err) { next(err); }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// GET /:id/access — effective per-project access for one user (admin only).
|
||||||
|
// Reuses authz.accessibleProjectIds (MAX over direct user grant + every group the
|
||||||
|
// user belongs to). `via` is 'direct' for a user grant, 'group:<name>' otherwise.
|
||||||
|
// When the effective level comes from several sources we report the direct grant
|
||||||
|
// if present, else the first contributing group.
|
||||||
|
router.get('/:id/access', requireAdmin, async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { rows: urows } = await pool.query(
|
||||||
|
`SELECT id, role FROM users WHERE id = $1`, [req.params.id]);
|
||||||
|
if (urows.length === 0) return res.status(404).json({ error: 'user not found' });
|
||||||
|
const target = urows[0];
|
||||||
|
|
||||||
|
const { rows: groups } = await pool.query(
|
||||||
|
`SELECT g.id, g.name
|
||||||
|
FROM user_groups ug JOIN groups g ON g.id = ug.group_id
|
||||||
|
WHERE ug.user_id = $1 ORDER BY g.name`, [target.id]);
|
||||||
|
|
||||||
|
// Admins bypass scoping — every project at 'edit', via their role.
|
||||||
|
const access = await accessibleProjectIds(target);
|
||||||
|
if (access.all) {
|
||||||
|
const { rows: projects } = await pool.query(
|
||||||
|
`SELECT id, name FROM projects ORDER BY name`);
|
||||||
|
return res.json({
|
||||||
|
projects: projects.map(p => ({
|
||||||
|
project_id: p.id, project_name: p.name, level: 'edit', via: 'direct',
|
||||||
|
})),
|
||||||
|
groups,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const ids = [...access.ids];
|
||||||
|
if (ids.length === 0) return res.json({ projects: [], groups });
|
||||||
|
|
||||||
|
// Resolve names + the source of each grant. groupNameById lets us label a
|
||||||
|
// group-sourced grant; a direct user grant always wins the `via` label.
|
||||||
|
const groupNameById = new Map(groups.map(g => [g.id, g.name]));
|
||||||
|
const { rows: grants } = await pool.query(
|
||||||
|
`SELECT pa.project_id, pa.subject_type, pa.subject_id, pa.level, p.name AS project_name
|
||||||
|
FROM project_access pa JOIN projects p ON p.id = pa.project_id
|
||||||
|
WHERE (pa.subject_type = 'user' AND pa.subject_id = $1)
|
||||||
|
OR (pa.subject_type = 'group' AND pa.subject_id IN (
|
||||||
|
SELECT group_id FROM user_groups WHERE user_id = $1
|
||||||
|
))`,
|
||||||
|
[target.id]);
|
||||||
|
|
||||||
|
const byProject = new Map();
|
||||||
|
for (const g of grants) {
|
||||||
|
const eff = access.levelByProject.get(g.project_id); // already the MAX
|
||||||
|
const via = g.subject_type === 'user'
|
||||||
|
? 'direct'
|
||||||
|
: 'group:' + (groupNameById.get(g.subject_id) || g.subject_id);
|
||||||
|
const prev = byProject.get(g.project_id);
|
||||||
|
// Keep a row only if it carries the effective level; prefer a direct grant
|
||||||
|
// when both a direct and a group grant hit the same level.
|
||||||
|
if (g.level === eff && (!prev || (prev.via !== 'direct' && via === 'direct'))) {
|
||||||
|
byProject.set(g.project_id, {
|
||||||
|
project_id: g.project_id, project_name: g.project_name, level: eff, via,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
projects: [...byProject.values()].sort((a, b) => a.project_name.localeCompare(b.project_name)),
|
||||||
|
groups,
|
||||||
|
});
|
||||||
|
} catch (err) { next(err); }
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST /:id/totp/disable — admin clears a locked-out user's 2FA WITHOUT their
|
||||||
|
// password (the self-service /auth/totp/disable needs the victim's own). Mirrors
|
||||||
|
// that handler's SQL but targets :id and skips the password check. Dev user blocked.
|
||||||
|
router.post('/:id/totp/disable', requireAdmin, async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
if (req.params.id === DEV_USER_ID) return res.status(400).json({ error: 'cannot edit dev user' });
|
||||||
|
const { rowCount } = await pool.query(
|
||||||
|
`UPDATE users SET totp_enabled = FALSE, totp_secret = NULL, totp_last_counter = 0
|
||||||
|
WHERE id = $1 AND id <> $2`,
|
||||||
|
[req.params.id, DEV_USER_ID]);
|
||||||
|
if (rowCount === 0) return res.status(404).json({ error: 'user not found' });
|
||||||
|
await pool.query(`DELETE FROM user_recovery_codes WHERE user_id = $1`, [req.params.id]);
|
||||||
|
res.status(204).end();
|
||||||
|
} catch (err) { next(err); }
|
||||||
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
|
|
||||||
import pool from './db/pool.js';
|
import pool from './db/pool.js';
|
||||||
import { syncToAmpp } from './routes/upload.js';
|
import { syncToAmpp } from './routes/upload.js';
|
||||||
import { restartChannel } from './routes/playout.js';
|
import { restartChannel, fireScteBreak } from './routes/playout.js';
|
||||||
import { INTERNAL_TOKEN } from './middleware/auth.js';
|
import { INTERNAL_TOKEN } from './middleware/auth.js';
|
||||||
|
|
||||||
const TICK_INTERVAL_MS = parseInt(process.env.SCHEDULER_TICK_MS || '15000', 10);
|
const TICK_INTERVAL_MS = parseInt(process.env.SCHEDULER_TICK_MS || '15000', 10);
|
||||||
|
|
@ -34,11 +34,7 @@ async function callSelf(path, method = 'POST') {
|
||||||
return res.json().catch(() => ({}));
|
return res.json().catch(() => ({}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Issue #103 — every mam-api replica runs the same tick on the same interval,
|
const SCHEDULER_LOCK_KEY = 8210301;
|
||||||
// so a multi-node deploy would double-fire recorder starts/stops. We guard
|
|
||||||
// the whole tick with a PG advisory lock (1 = scheduler) so exactly one
|
|
||||||
// replica processes a given interval. Pure-Postgres, no extra infra.
|
|
||||||
const SCHEDULER_LOCK_KEY = 8210301; // arbitrary, must be stable across replicas
|
|
||||||
|
|
||||||
async function tryAcquireSchedulerLock(client) {
|
async function tryAcquireSchedulerLock(client) {
|
||||||
const r = await client.query('SELECT pg_try_advisory_lock($1) AS got', [SCHEDULER_LOCK_KEY]);
|
const r = await client.query('SELECT pg_try_advisory_lock($1) AS got', [SCHEDULER_LOCK_KEY]);
|
||||||
|
|
@ -57,14 +53,9 @@ async function tick() {
|
||||||
try {
|
try {
|
||||||
haveLock = await tryAcquireSchedulerLock(client);
|
haveLock = await tryAcquireSchedulerLock(client);
|
||||||
if (!haveLock) {
|
if (!haveLock) {
|
||||||
// Another replica is processing this interval — bail silently.
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1) Atomically claim pending schedules whose window has opened. The
|
|
||||||
// UPDATE...RETURNING flips status to 'running' in the same statement
|
|
||||||
// so even if another replica got past the lock (it can't, but
|
|
||||||
// belt-and-braces) each row can only be claimed once.
|
|
||||||
const dueStart = await client.query(
|
const dueStart = await client.query(
|
||||||
`UPDATE recorder_schedules
|
`UPDATE recorder_schedules
|
||||||
SET status = 'starting', updated_at = NOW()
|
SET status = 'starting', updated_at = NOW()
|
||||||
|
|
@ -97,7 +88,6 @@ async function tick() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) Atomically claim running schedules whose window has closed.
|
|
||||||
const dueStop = await client.query(
|
const dueStop = await client.query(
|
||||||
`UPDATE recorder_schedules
|
`UPDATE recorder_schedules
|
||||||
SET status = 'stopping', updated_at = NOW()
|
SET status = 'stopping', updated_at = NOW()
|
||||||
|
|
@ -120,7 +110,6 @@ async function tick() {
|
||||||
console.log(`[scheduler] stopped schedule "${s.name}" on recorder ${s.recorder_id}`);
|
console.log(`[scheduler] stopped schedule "${s.name}" on recorder ${s.recorder_id}`);
|
||||||
await enqueueNextOccurrence(s, client);
|
await enqueueNextOccurrence(s, client);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Stop failed — flag as failed but don't keep trying forever.
|
|
||||||
await client.query(
|
await client.query(
|
||||||
`UPDATE recorder_schedules
|
`UPDATE recorder_schedules
|
||||||
SET status = 'failed', error_message = $2, updated_at = NOW()
|
SET status = 'failed', error_message = $2, updated_at = NOW()
|
||||||
|
|
@ -131,7 +120,6 @@ async function tick() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) If a schedule was cancelled while running, stop the recorder.
|
|
||||||
const cancelledRunning = await client.query(
|
const cancelledRunning = await client.query(
|
||||||
`SELECT s.* FROM recorder_schedules s
|
`SELECT s.* FROM recorder_schedules s
|
||||||
JOIN recorders r ON r.id = s.recorder_id
|
JOIN recorders r ON r.id = s.recorder_id
|
||||||
|
|
@ -147,9 +135,6 @@ async function tick() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4) Mark stale live assets as 'error' (#66).
|
|
||||||
// If a capture container crashes without calling mark-empty/mark-complete,
|
|
||||||
// the asset row stays status='live' indefinitely. Timeout after 2 hours.
|
|
||||||
const LIVE_TIMEOUT_MINUTES = parseInt(process.env.LIVE_ASSET_TIMEOUT_MINUTES || '120', 10);
|
const LIVE_TIMEOUT_MINUTES = parseInt(process.env.LIVE_ASSET_TIMEOUT_MINUTES || '120', 10);
|
||||||
const staleResult = await client.query(
|
const staleResult = await client.query(
|
||||||
`UPDATE assets
|
`UPDATE assets
|
||||||
|
|
@ -166,9 +151,6 @@ async function tick() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5) AMPP sync retry (#77). Pick up any pending/failed rows whose
|
|
||||||
// next-attempt time has arrived and retry them. Cap per tick so we
|
|
||||||
// don't burn budget on a single rough interval.
|
|
||||||
const ampps = await client.query(
|
const ampps = await client.query(
|
||||||
`SELECT id, project_id, bin_id FROM assets
|
`SELECT id, project_id, bin_id FROM assets
|
||||||
WHERE ampp_sync_status IN ('pending', 'failed')
|
WHERE ampp_sync_status IN ('pending', 'failed')
|
||||||
|
|
@ -181,11 +163,6 @@ async function tick() {
|
||||||
await syncToAmpp(row.id, row.project_id, row.bin_id);
|
await syncToAmpp(row.id, row.project_id, row.bin_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6) Playout channel health checks. Ping each running channel's sidecar
|
|
||||||
// /status; on success bump last_heartbeat_at, on failure increment a
|
|
||||||
// transient miss counter (in playout_sidecars.last_heartbeat_at age).
|
|
||||||
// Three consecutive misses → auto-restart on a healthy node (non-
|
|
||||||
// decklink), or alert-only for decklink.
|
|
||||||
await playoutHealthTick(client);
|
await playoutHealthTick(client);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[scheduler] tick error:', err);
|
console.error('[scheduler] tick error:', err);
|
||||||
|
|
@ -285,7 +262,6 @@ async function playoutHealthTick(client) {
|
||||||
FROM playout_channels WHERE status = 'running'`
|
FROM playout_channels WHERE status = 'running'`
|
||||||
));
|
));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Migration 029 may not be applied yet — bail silently rather than crash.
|
|
||||||
if (err.code === '42P01') return;
|
if (err.code === '42P01') return;
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
@ -313,6 +289,32 @@ async function playoutHealthTick(client) {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(`[scheduler] as-run write failed for ${ch.id}: ${e.message}`);
|
console.warn(`[scheduler] as-run write failed for ${ch.id}: ${e.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SCTE-35: fire any pending scheduled breaks now due. Position-based
|
||||||
|
// breaks (playlist_pos) fire when the engine reaches that item; wall-clock
|
||||||
|
// breaks fire at scheduled_at. Failures mark the break cancelled so a bad
|
||||||
|
// break never wedges the sweep.
|
||||||
|
try {
|
||||||
|
const { rows: due } = await client.query(
|
||||||
|
`SELECT * FROM playout_scte_breaks
|
||||||
|
WHERE channel_id = $1 AND status = 'pending'
|
||||||
|
AND ( (scheduled_at IS NOT NULL AND scheduled_at <= NOW())
|
||||||
|
OR (playlist_pos IS NOT NULL AND playlist_pos <= $2) )
|
||||||
|
ORDER BY created_at ASC`,
|
||||||
|
[ch.id, (engine && engine.currentIndex >= 0) ? engine.currentIndex : -1]
|
||||||
|
);
|
||||||
|
for (const brk of due) {
|
||||||
|
try { await fireScteBreak(ch, brk); }
|
||||||
|
catch (e2) {
|
||||||
|
console.warn(`[scheduler] scte fire failed for break ${brk.id}: ${e2.message}`);
|
||||||
|
await client.query(
|
||||||
|
`UPDATE playout_scte_breaks SET status = 'cancelled', updated_at = NOW() WHERE id = $1`,
|
||||||
|
[brk.id]).catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`[scheduler] scte sweep failed for ${ch.id}: ${e.message}`);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// When last_heartbeat_at is NULL (channel just spawned), fall back to
|
// When last_heartbeat_at is NULL (channel just spawned), fall back to
|
||||||
// updated_at (set to NOW() by spawnChannelSidecar). This prevents a
|
// updated_at (set to NOW() by spawnChannelSidecar). This prevents a
|
||||||
|
|
@ -321,7 +323,7 @@ async function playoutHealthTick(client) {
|
||||||
const baseline = ch.last_heartbeat_at || ch.updated_at;
|
const baseline = ch.last_heartbeat_at || ch.updated_at;
|
||||||
const lastSeen = baseline ? new Date(baseline).getTime() : Date.now();
|
const lastSeen = baseline ? new Date(baseline).getTime() : Date.now();
|
||||||
const ageMs = Date.now() - lastSeen;
|
const ageMs = Date.now() - lastSeen;
|
||||||
if (ageMs < TIMEOUT_MS) continue; // not yet 3 misses
|
if (ageMs < TIMEOUT_MS) continue;
|
||||||
|
|
||||||
if (ch.output_type === 'decklink') {
|
if (ch.output_type === 'decklink') {
|
||||||
await client.query(
|
await client.query(
|
||||||
|
|
@ -334,8 +336,6 @@ async function playoutHealthTick(client) {
|
||||||
|
|
||||||
console.warn(`[scheduler] failover: channel ${ch.id} unreachable (${err.message}), restart #${ch.restart_count + 1}`);
|
console.warn(`[scheduler] failover: channel ${ch.id} unreachable (${err.message}), restart #${ch.restart_count + 1}`);
|
||||||
try {
|
try {
|
||||||
// restartChannel re-places the channel on a healthy node AND spawns the
|
|
||||||
// new sidecar directly (shared helper) — no /start self-call needed.
|
|
||||||
const res = await restartChannel(ch.id);
|
const res = await restartChannel(ch.id);
|
||||||
if (res.restarted) {
|
if (res.restarted) {
|
||||||
console.log(`[scheduler] failover: channel ${ch.id} re-placed on node ${res.new_node_id}`);
|
console.log(`[scheduler] failover: channel ${ch.id} re-placed on node ${res.new_node_id}`);
|
||||||
|
|
@ -352,8 +352,6 @@ async function playoutHealthTick(client) {
|
||||||
export function startSchedulerLoop() {
|
export function startSchedulerLoop() {
|
||||||
if (_interval) return;
|
if (_interval) return;
|
||||||
console.log(`[scheduler] tick loop started (interval=${TICK_INTERVAL_MS}ms)`);
|
console.log(`[scheduler] tick loop started (interval=${TICK_INTERVAL_MS}ms)`);
|
||||||
// Fire once on startup so a window that opened while the API was down
|
|
||||||
// doesn't have to wait a full interval.
|
|
||||||
setTimeout(() => tick().catch(() => {}), 2000);
|
setTimeout(() => tick().catch(() => {}), 2000);
|
||||||
_interval = setInterval(() => tick().catch(() => {}), TICK_INTERVAL_MS);
|
_interval = setInterval(() => tick().catch(() => {}), TICK_INTERVAL_MS);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,170 @@
|
||||||
import http from 'http';
|
import http from 'http';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
import { spawn } from 'child_process';
|
||||||
|
|
||||||
const MAM_API_URL = (process.env.MAM_API_URL || 'http://localhost:3000').replace(/\/$/, '');
|
const MAM_API_URL = (process.env.MAM_API_URL || 'http://localhost:3000').replace(/\/$/, '');
|
||||||
const NODE_TOKEN = process.env.NODE_TOKEN || '';
|
const NODE_TOKEN = process.env.NODE_TOKEN || '';
|
||||||
const NODE_ROLE = process.env.NODE_ROLE || 'worker';
|
const NODE_ROLE = process.env.NODE_ROLE || 'worker';
|
||||||
|
// Cluster identity. The heartbeat keys cluster_nodes on hostname (ON CONFLICT
|
||||||
|
// (hostname)), so two machines reporting the SAME os.hostname() clobber each
|
||||||
|
// other's row — exactly what happens with cloned VMs that share /etc/hostname
|
||||||
|
// (e.g. two boxes both named "zampp1"). The capture node's DeckLink capability
|
||||||
|
// then lands on the wrong row and gets overwritten by the primary's cardless
|
||||||
|
// heartbeat, so the recorder UI shows "No SDI devices auto-detected".
|
||||||
|
// NODE_NAME (set per-node by onboard-node.sh / the node's .env) overrides
|
||||||
|
// os.hostname() so identity is explicit and collision-proof. Falls back to the
|
||||||
|
// OS hostname when unset, preserving existing single-host behaviour.
|
||||||
|
const NODE_NAME = process.env.NODE_NAME || os.hostname();
|
||||||
const AGENT_PORT = parseInt(process.env.AGENT_PORT || '7436', 10);
|
const AGENT_PORT = parseInt(process.env.AGENT_PORT || '7436', 10);
|
||||||
const HEARTBEAT_MS = parseInt(process.env.HEARTBEAT_MS || '30000', 10);
|
const HEARTBEAT_MS = parseInt(process.env.HEARTBEAT_MS || '30000', 10);
|
||||||
const LIVE_DIR = process.env.LIVE_DIR || '/mnt/NVME/MAM/wild-dragon-live';
|
const LIVE_DIR = process.env.LIVE_DIR || '/mnt/NVME/MAM/wild-dragon-live';
|
||||||
const VERSION = '1.3.0';
|
// Host path to the checked-out repo (onboard-node.sh clones to /opt/wild-dragon).
|
||||||
|
// The driver-install container bind-mounts this so install-driver.sh can read
|
||||||
|
// sdk/<vendor>/ and run from deploy/. Overridable for non-standard layouts.
|
||||||
|
const REPO_DIR = process.env.REPO_DIR || '/opt/wild-dragon';
|
||||||
|
const VERSION = '1.4.0';
|
||||||
|
|
||||||
|
// Capture-driver vendor allowlist. NOTHING outside this set is ever passed to
|
||||||
|
// the host installer — the value is only ever used to pick a script arg, never
|
||||||
|
// interpolated into a shell string.
|
||||||
|
const DRIVER_VENDORS = ['blackmagic', 'aja', 'deltacast', 'ndi'];
|
||||||
|
|
||||||
|
// ── Deltacast board-open mutex (legacy — no longer used) ─────────────────
|
||||||
|
// The per-sidecar board-open race is eliminated by the shared bridge daemon
|
||||||
|
// (deltacast-bridge). This mutex is kept but acquireDcLock() is never called
|
||||||
|
// for deltacast sidecars; they wait for the bridge FIFOs instead.
|
||||||
|
const DELTACAST_STAGGER_MS = parseInt(process.env.DELTACAST_START_STAGGER_MS || '3500', 10);
|
||||||
|
let _dcMutex = Promise.resolve();
|
||||||
|
|
||||||
|
function acquireDcLock() {
|
||||||
|
let release;
|
||||||
|
const next = new Promise(resolve => { release = resolve; });
|
||||||
|
const wait = _dcMutex;
|
||||||
|
_dcMutex = _dcMutex.then(() => next);
|
||||||
|
return wait.then(() => release);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Deltacast shared bridge daemon ────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// ONE deltacast-bridge process runs on the HOST (not inside a container) and
|
||||||
|
// opens the board handle exactly once, serving all requested ports via FIFOs
|
||||||
|
// in /dev/shm/deltacast/. This eliminates the BufMngr.c:781 OOB fault caused
|
||||||
|
// by concurrent VHD_OpenBoardHandle calls.
|
||||||
|
//
|
||||||
|
// Lifecycle:
|
||||||
|
// - First deltacast sidecar start → bridge launched with all configured ports.
|
||||||
|
// - Subsequent starts → sidecar reads existing FIFOs; bridge unchanged.
|
||||||
|
// - Last deltacast sidecar stop → bridge killed.
|
||||||
|
// - Bridge unexpected exit → _dcBridge reset; next sidecar re-launches it.
|
||||||
|
//
|
||||||
|
// DELTACAST_PIPE_DIR (default /dev/shm/deltacast): FIFO directory, bind-mounted
|
||||||
|
// into each deltacast sidecar so ffmpeg can read the FIFOs.
|
||||||
|
// DELTACAST_BRIDGE_BIN (default deltacast-bridge): host path to the binary.
|
||||||
|
// Typically /usr/local/bin/deltacast-bridge after `make install` from the SDK
|
||||||
|
// build, or set to the build-dir path for development.
|
||||||
|
// DELTACAST_PORTS (csv, e.g. "0,1,2,4,7"): ports the bridge opens at launch.
|
||||||
|
// Defaults to all 8 ports (0-7) so any sidecar port combination is covered.
|
||||||
|
|
||||||
|
const DC_PIPE_DIR = process.env.DELTACAST_PIPE_DIR || '/dev/shm/deltacast';
|
||||||
|
const DC_BRIDGE_BIN = process.env.DELTACAST_BRIDGE_BIN || 'deltacast-bridge';
|
||||||
|
const DC_PORTS_CSV = process.env.DELTACAST_PORTS || '0,1,2,3,4,5,6,7';
|
||||||
|
const DC_BOARD = process.env.DELTACAST_BOARD || '0';
|
||||||
|
|
||||||
|
let _dcBridge = null; // ChildProcess | null
|
||||||
|
let _dcSidecarCount = 0; // active deltacast sidecars on this node
|
||||||
|
// Map containerId -> sourceType so stop() can decrement the deltacast counter.
|
||||||
|
const _containerSourceType = new Map();
|
||||||
|
// port -> fmt JSON from bridge stderr (inject into sidecar env)
|
||||||
|
const _dcPortFmt = new Map();
|
||||||
|
|
||||||
|
function _dcBridgeRunning() {
|
||||||
|
return _dcBridge !== null && _dcBridge.exitCode === null && _dcBridge.signalCode === null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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('exit', (code, sig) => {
|
||||||
|
console.error(`[dc-bridge] exited code=${code} signal=${sig}`);
|
||||||
|
_dcBridge = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
_dcBridge = proc;
|
||||||
|
console.log(`[dc-bridge] pid=${proc.pid} board=${DC_BOARD} ports=${DC_PORTS_CSV}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopDeltacastBridge() {
|
||||||
|
if (!_dcBridgeRunning()) return;
|
||||||
|
console.log('[dc-bridge] stopping (no active deltacast sidecars)');
|
||||||
|
try { _dcBridge.kill('SIGTERM'); } catch (_) {}
|
||||||
|
// Give it 5s to clean up, then SIGKILL.
|
||||||
|
const proc = _dcBridge;
|
||||||
|
setTimeout(() => {
|
||||||
|
try { if (proc.exitCode === null) proc.kill('SIGKILL'); } catch (_) {}
|
||||||
|
}, 5000);
|
||||||
|
_dcBridge = null;
|
||||||
|
}
|
||||||
|
|
||||||
// Pick the host's LAN IP. Inside a bridge-mode container,
|
// Pick the host's LAN IP. Inside a bridge-mode container,
|
||||||
// os.networkInterfaces() returns the container's docker-bridge IP (172.x),
|
// os.networkInterfaces() returns the container's docker-bridge IP (172.x),
|
||||||
|
|
@ -93,6 +249,11 @@ async function handleSidecarStart(body, res) {
|
||||||
// (ProRes, DNxHR, libx264) don't need it and it avoids a hard dep on the
|
// (ProRes, DNxHR, libx264) don't need it and it avoids a hard dep on the
|
||||||
// NVIDIA container runtime on nodes that have no GPU.
|
// NVIDIA container runtime on nodes that have no GPU.
|
||||||
useGpu = false,
|
useGpu = false,
|
||||||
|
// Issue #167 — optional per-recorder GPU affinity. When set to a GPU
|
||||||
|
// UUID (e.g. "GPU-xxxx") or a numeric index, the sidecar is pinned to
|
||||||
|
// that single device via NVIDIA_VISIBLE_DEVICES instead of "all". null /
|
||||||
|
// undefined keeps the legacy "all" behavior (expose every GPU).
|
||||||
|
gpuUuid = null,
|
||||||
} = body;
|
} = body;
|
||||||
|
|
||||||
const binds = [`${LIVE_DIR}:/live`];
|
const binds = [`${LIVE_DIR}:/live`];
|
||||||
|
|
@ -103,17 +264,22 @@ async function handleSidecarStart(body, res) {
|
||||||
try {
|
try {
|
||||||
const dcEntries = fs.readdirSync('/dev').filter(n => /^deltacast\d+$/.test(n));
|
const dcEntries = fs.readdirSync('/dev').filter(n => /^deltacast\d+$/.test(n));
|
||||||
for (const d of dcEntries) binds.push(`/dev/${d}:/dev/${d}`);
|
for (const d of dcEntries) binds.push(`/dev/${d}:/dev/${d}`);
|
||||||
|
// VideoMaster SDK needs the board IPC shared-memory segment mounted too.
|
||||||
|
if (fs.existsSync('/dev/shm/deltacast')) binds.push('/dev/shm/deltacast:/dev/shm/deltacast');
|
||||||
} catch (_) { /* /dev always exists */ }
|
} catch (_) { /* /dev always exists */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the sidecar environment, injecting NVIDIA vars when GPU is requested.
|
// Build the sidecar environment, injecting NVIDIA vars when GPU is requested.
|
||||||
const sidecarEnv = [...env, `PORT=${capturePort}`];
|
const sidecarEnv = [...env, `PORT=${capturePort}`];
|
||||||
if (useGpu) {
|
if (useGpu) {
|
||||||
// NVIDIA_VISIBLE_DEVICES=all exposes every GPU on the host.
|
// Issue #167 — per-recorder GPU affinity. A gpuUuid (UUID string or
|
||||||
// For a single-GPU node (zampp2 / L4) this is equivalent to pinning GPU 0.
|
// numeric index) pins the sidecar to exactly that device; otherwise
|
||||||
// When we later store per-recorder GPU affinity in the DB we can pass a
|
// NVIDIA_VISIBLE_DEVICES=all exposes every GPU on the host (legacy
|
||||||
// specific UUID here instead.
|
// behavior — for a single-GPU node like zampp2 / L4 this equals GPU 0).
|
||||||
sidecarEnv.push('NVIDIA_VISIBLE_DEVICES=all');
|
const visibleDevices = (gpuUuid != null && String(gpuUuid).trim() !== '')
|
||||||
|
? String(gpuUuid).trim()
|
||||||
|
: 'all';
|
||||||
|
sidecarEnv.push(`NVIDIA_VISIBLE_DEVICES=${visibleDevices}`);
|
||||||
sidecarEnv.push('NVIDIA_DRIVER_CAPABILITIES=video,compute,utility');
|
sidecarEnv.push('NVIDIA_DRIVER_CAPABILITIES=video,compute,utility');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -137,22 +303,60 @@ async function handleSidecarStart(body, res) {
|
||||||
HostConfig: hostConfig,
|
HostConfig: hostConfig,
|
||||||
};
|
};
|
||||||
|
|
||||||
const createRes = await dockerApi('POST', '/containers/create', spec);
|
// Deltacast: ensure the shared bridge daemon is running on the HOST before
|
||||||
if (createRes.status !== 201) {
|
// starting the sidecar. The sidecar reads FIFOs produced by the bridge;
|
||||||
return jsonResponse(res, 502, { error: 'Failed to create container', details: createRes.data });
|
// it does NOT open the board handle itself (no BufMngr.c:781 race).
|
||||||
|
if (sourceType === 'deltacast') {
|
||||||
|
_dcSidecarCount++;
|
||||||
|
startDeltacastBridge();
|
||||||
|
// Inject per-port signal format so capture-manager uses real dimensions/fps
|
||||||
|
const _srcCfg = (env.find(e => e.startsWith('SOURCE_CONFIG=')) || '').slice(14);
|
||||||
|
let _portNum = NaN;
|
||||||
|
try { _portNum = JSON.parse(_srcCfg).port; } catch (_) {}
|
||||||
|
if (Number.isFinite(_portNum) && _dcPortFmt.has(_portNum)) {
|
||||||
|
const _fmt = _dcPortFmt.get(_portNum);
|
||||||
|
const _fps = (_fmt.fps_den && _fmt.fps_den !== 1) ? `${_fmt.fps_num}/${_fmt.fps_den}` : String(_fmt.fps_num);
|
||||||
|
sidecarEnv.push(`DELTACAST_VIDEO_SIZE=${_fmt.width}x${_fmt.height}`);
|
||||||
|
sidecarEnv.push(`DELTACAST_FRAMERATE=${_fps}`);
|
||||||
|
sidecarEnv.push(`DELTACAST_INTERLACED=${_fmt.interlaced ? '1' : '0'}`);
|
||||||
|
console.log(`[dc-bridge] port ${_portNum} fmt: ${_fmt.width}x${_fmt.height} ${_fps} interlaced=${_fmt.interlaced}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const containerId = createRes.data.Id;
|
let containerId;
|
||||||
const _u = (env.find(e => e.startsWith('MAM_API_URL=')) || '').slice(12);
|
try {
|
||||||
const _tok = env.some(e => e.startsWith('MAM_API_TOKEN=') && e.length > 14);
|
const createRes = await dockerApi('POST', '/containers/create', spec);
|
||||||
console.log(`[sidecar-start] ${containerId} image=${image} src=${sourceType} MAM_API_URL=${_u} token=${_tok}`);
|
if (createRes.status !== 201) {
|
||||||
const startRes = await dockerApi('POST', `/containers/${containerId}/start`);
|
if (sourceType === 'deltacast') {
|
||||||
if (startRes.status !== 204) {
|
_dcSidecarCount--;
|
||||||
await dockerApi('DELETE', `/containers/${containerId}?force=true`).catch(() => {});
|
if (_dcSidecarCount <= 0) { _dcSidecarCount = 0; stopDeltacastBridge(); }
|
||||||
return jsonResponse(res, 502, { error: 'Failed to start container', details: startRes.data });
|
}
|
||||||
}
|
return jsonResponse(res, 502, { error: 'Failed to create container', details: createRes.data });
|
||||||
|
}
|
||||||
|
|
||||||
jsonResponse(res, 201, { containerId, capturePort });
|
containerId = createRes.data.Id;
|
||||||
|
const _u = (env.find(e => e.startsWith('MAM_API_URL=')) || '').slice(12);
|
||||||
|
const _tok = env.some(e => e.startsWith('MAM_API_TOKEN=') && e.length > 14);
|
||||||
|
console.log(`[sidecar-start] ${containerId} image=${image} src=${sourceType} MAM_API_URL=${_u} token=${_tok}`);
|
||||||
|
const startRes = await dockerApi('POST', `/containers/${containerId}/start`);
|
||||||
|
if (startRes.status !== 204) {
|
||||||
|
if (sourceType === 'deltacast') {
|
||||||
|
_dcSidecarCount--;
|
||||||
|
if (_dcSidecarCount <= 0) { _dcSidecarCount = 0; stopDeltacastBridge(); }
|
||||||
|
}
|
||||||
|
await dockerApi('DELETE', `/containers/${containerId}?force=true`).catch(() => {});
|
||||||
|
return jsonResponse(res, 502, { error: 'Failed to start container', details: startRes.data });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sourceType === 'deltacast') _containerSourceType.set(containerId, 'deltacast');
|
||||||
|
jsonResponse(res, 201, { containerId, capturePort });
|
||||||
|
} catch (err) {
|
||||||
|
if (sourceType === 'deltacast') {
|
||||||
|
_dcSidecarCount--;
|
||||||
|
if (_dcSidecarCount <= 0) { _dcSidecarCount = 0; stopDeltacastBridge(); }
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
jsonResponse(res, 500, { error: err.message });
|
jsonResponse(res, 500, { error: err.message });
|
||||||
}
|
}
|
||||||
|
|
@ -189,6 +393,19 @@ async function handleSidecarStop(containerId, res) {
|
||||||
console.log(`[sidecar-stop] ==== capture logs for ${containerId} ====\n${logs}\n[sidecar-stop] ==== end logs ====`);
|
console.log(`[sidecar-stop] ==== capture logs for ${containerId} ====\n${logs}\n[sidecar-stop] ==== end logs ====`);
|
||||||
// Container has now exited gracefully (or hit the 180s cap); remove it.
|
// Container has now exited gracefully (or hit the 180s cap); remove it.
|
||||||
await dockerApi('DELETE', `/containers/${containerId}?force=true`).catch(() => {});
|
await dockerApi('DELETE', `/containers/${containerId}?force=true`).catch(() => {});
|
||||||
|
|
||||||
|
// Deltacast bridge lifecycle: decrement sidecar count; stop bridge when last.
|
||||||
|
if (_containerSourceType.get(containerId) === 'deltacast') {
|
||||||
|
_containerSourceType.delete(containerId);
|
||||||
|
_dcSidecarCount--;
|
||||||
|
if (_dcSidecarCount <= 0) {
|
||||||
|
_dcSidecarCount = 0;
|
||||||
|
stopDeltacastBridge();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_containerSourceType.delete(containerId);
|
||||||
|
}
|
||||||
|
|
||||||
jsonResponse(res, 200, { ok: true });
|
jsonResponse(res, 200, { ok: true });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`[sidecar-stop] error: ${err.message}`);
|
console.error(`[sidecar-stop] error: ${err.message}`);
|
||||||
|
|
@ -227,6 +444,147 @@ async function handleSidecarStatus(containerId, res) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Agent auth ────────────────────────────────────────────────────────────
|
||||||
|
// When NODE_TOKEN is configured, privileged control endpoints (driver install)
|
||||||
|
// require a matching `Authorization: Bearer <NODE_TOKEN>`. mam-api forwards the
|
||||||
|
// node's stored token. If NODE_TOKEN is empty (dev), auth is not enforced.
|
||||||
|
function checkAgentAuth(req) {
|
||||||
|
if (!NODE_TOKEN) return true;
|
||||||
|
const hdr = req.headers['authorization'] || '';
|
||||||
|
const m = /^Bearer\s+(.+)$/i.exec(hdr);
|
||||||
|
return !!m && m[1] === NODE_TOKEN;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Driver/SDK install ────────────────────────────────────────────────────
|
||||||
|
// Probe host presence of each capture-driver vendor. Mirrors the detection the
|
||||||
|
// install script uses, so the UI can show "installed / not installed" without
|
||||||
|
// running the installer. Best-effort: every probe is guarded.
|
||||||
|
function probeDriverStatus() {
|
||||||
|
const out = {};
|
||||||
|
|
||||||
|
// blackmagic — kernel module + /dev/blackmagic device tree.
|
||||||
|
let bmLoaded = false;
|
||||||
|
try { bmLoaded = fs.readFileSync('/proc/modules', 'utf8').split('\n').some(l => /^blackmagic\b/.test(l)); } catch (_) {}
|
||||||
|
let bmDev = false;
|
||||||
|
try { bmDev = fs.existsSync('/dev/blackmagic') && fs.readdirSync('/dev/blackmagic').length > 0; } catch (_) {}
|
||||||
|
out.blackmagic = { installed: bmLoaded || bmDev, module_loaded: bmLoaded, device_present: bmDev };
|
||||||
|
|
||||||
|
// aja — ajantv2 kernel module.
|
||||||
|
let ajaLoaded = false;
|
||||||
|
try { ajaLoaded = fs.readFileSync('/proc/modules', 'utf8').split('\n').some(l => /ajantv2/.test(l)); } catch (_) {}
|
||||||
|
out.aja = { installed: ajaLoaded, module_loaded: ajaLoaded };
|
||||||
|
|
||||||
|
// deltacast — videomaster module or /dev/deltacast* node.
|
||||||
|
let dcLoaded = false;
|
||||||
|
try { dcLoaded = fs.readFileSync('/proc/modules', 'utf8').split('\n').some(l => /videomaster/.test(l)); } catch (_) {}
|
||||||
|
let dcDev = false;
|
||||||
|
try { dcDev = fs.readdirSync('/dev').some(n => /^deltacast\d+$/.test(n)); } catch (_) {}
|
||||||
|
out.deltacast = { installed: dcLoaded || dcDev, module_loaded: dcLoaded, device_present: dcDev };
|
||||||
|
|
||||||
|
// ndi — user-space libs only. Look in the install target + common lib dirs.
|
||||||
|
let ndiPresent = false;
|
||||||
|
try {
|
||||||
|
for (const dir of ['/opt/ndi-lib', '/usr/local/lib', '/usr/lib/x86_64-linux-gnu']) {
|
||||||
|
let entries = [];
|
||||||
|
try { entries = fs.readdirSync(dir); } catch (_) { continue; }
|
||||||
|
if (entries.some(n => /^libndi\.so/.test(n))) { ndiPresent = true; break; }
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
out.ndi = { installed: ndiPresent, libs_present: ndiPresent };
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDriverStatus(res) {
|
||||||
|
try {
|
||||||
|
jsonResponse(res, 200, { kernel: os.release(), vendors: probeDriverStatus() });
|
||||||
|
} catch (err) {
|
||||||
|
jsonResponse(res, 500, { error: err.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run install-driver.sh <vendor> inside a one-shot PRIVILEGED ubuntu container.
|
||||||
|
// The repo is bind-mounted read-only at /repo; host kernel paths are mounted so
|
||||||
|
// dkms/modprobe/ldconfig affect the host. Logs are streamed back to the caller.
|
||||||
|
async function handleDriverInstall(body, res) {
|
||||||
|
const vendor = String(body?.vendor || '').toLowerCase();
|
||||||
|
if (!DRIVER_VENDORS.includes(vendor)) {
|
||||||
|
return jsonResponse(res, 400, { error: `Invalid vendor (allowed: ${DRIVER_VENDORS.join(', ')})` });
|
||||||
|
}
|
||||||
|
|
||||||
|
let containerId;
|
||||||
|
try {
|
||||||
|
// Host paths the installer needs to reach the host kernel:
|
||||||
|
// /lib/modules,/usr/src,/boot → DKMS / module build + install
|
||||||
|
// /dev → device-node visibility + udev
|
||||||
|
// The repo (sdk/<vendor>/ + deploy/install-driver.sh) is mounted read-only.
|
||||||
|
const binds = [
|
||||||
|
`${REPO_DIR}:/repo:ro`,
|
||||||
|
'/lib/modules:/lib/modules',
|
||||||
|
'/usr/src:/usr/src',
|
||||||
|
'/boot:/boot',
|
||||||
|
'/dev:/dev',
|
||||||
|
// NDI install target lives under /opt; expose host /opt so libs land on host.
|
||||||
|
'/opt:/opt',
|
||||||
|
];
|
||||||
|
|
||||||
|
const spec = {
|
||||||
|
Image: 'ubuntu:22.04',
|
||||||
|
// NOTE: vendor is a value from DRIVER_VENDORS only — never arbitrary input.
|
||||||
|
// Passed as a distinct argv element (Cmd array), not a shell string.
|
||||||
|
Cmd: ['bash', '/repo/deploy/install-driver.sh', vendor],
|
||||||
|
Env: [`REPO_DIR=/repo`],
|
||||||
|
WorkingDir: '/repo',
|
||||||
|
HostConfig: {
|
||||||
|
Privileged: true,
|
||||||
|
NetworkMode: 'host',
|
||||||
|
Binds: binds,
|
||||||
|
AutoRemove: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const createRes = await dockerApi('POST', '/containers/create', spec);
|
||||||
|
if (createRes.status !== 201) {
|
||||||
|
return jsonResponse(res, 502, { error: 'Failed to create install container', details: createRes.data });
|
||||||
|
}
|
||||||
|
containerId = createRes.data.Id;
|
||||||
|
console.log(`[driver-install] ${containerId} vendor=${vendor}`);
|
||||||
|
|
||||||
|
const startRes = await dockerApi('POST', `/containers/${containerId}/start`);
|
||||||
|
if (startRes.status !== 204) {
|
||||||
|
await dockerApi('DELETE', `/containers/${containerId}?force=true`).catch(() => {});
|
||||||
|
return jsonResponse(res, 502, { error: 'Failed to start install container', details: startRes.data });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the install to finish (DKMS builds can take a minute+).
|
||||||
|
let exitCode = null;
|
||||||
|
for (let i = 0; i < 600; i++) {
|
||||||
|
await new Promise(r => setTimeout(r, 1000));
|
||||||
|
const inspect = await dockerApi('GET', `/containers/${containerId}/json`);
|
||||||
|
const state = inspect.data?.State;
|
||||||
|
if (state && !state.Running) { exitCode = state.ExitCode; break; }
|
||||||
|
}
|
||||||
|
|
||||||
|
const logs = await fetchContainerLogs(containerId);
|
||||||
|
const rebootRequired = /REBOOT_REQUIRED=1/.test(logs);
|
||||||
|
const ok = exitCode === 0;
|
||||||
|
jsonResponse(res, ok ? 200 : 500, {
|
||||||
|
ok,
|
||||||
|
vendor,
|
||||||
|
exitCode,
|
||||||
|
rebootRequired,
|
||||||
|
logs,
|
||||||
|
status: probeDriverStatus()[vendor] || null,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
jsonResponse(res, 500, { error: err.message });
|
||||||
|
} finally {
|
||||||
|
if (containerId) {
|
||||||
|
await dockerApi('DELETE', `/containers/${containerId}?force=true`).catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── CPU sampling (500ms window) ───────────────────────────────────────────
|
// ── CPU sampling (500ms window) ───────────────────────────────────────────
|
||||||
function sampleCpu() {
|
function sampleCpu() {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
|
|
@ -247,21 +605,39 @@ function sampleCpu() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// -- Live GPU utilization sampling -----------------------------------------
|
// -- Live GPU / NVENC encode telemetry sampling -----------------------------
|
||||||
// Spawns a short-lived nvidia container via Docker API on each heartbeat call.
|
// Spawns a short-lived nvidia container via Docker API on each heartbeat call.
|
||||||
// Returns array of { index, util_pct, mem_used_mb, mem_total_mb } per GPU,
|
// Returns array of { index, util_pct, enc_util_pct, mem_used_mb, mem_total_mb,
|
||||||
// or [] if no GPUs / nvidia runtime unavailable.
|
// nvenc_sessions } per GPU, or [] if no GPUs / nvidia runtime unavailable.
|
||||||
|
//
|
||||||
|
// Two nvidia-smi queries are run inside one container via `sh -c`, each guarded
|
||||||
|
// with `|| true` so a query unsupported on a given driver/GPU (e.g. older cards
|
||||||
|
// that don't expose utilization.encoder) doesn't abort the whole sample:
|
||||||
|
// 1. --query-gpu → per-GPU gpu/encoder util + memory
|
||||||
|
// 2. --query-compute-apps → pid,used_memory,gpu_uuid for live processes; we
|
||||||
|
// count rows per GPU as an NVENC/compute "session" approximation. Marked
|
||||||
|
// with a SEP line so the two CSV blocks can be told apart in the log.
|
||||||
async function sampleGpuUtil() {
|
async function sampleGpuUtil() {
|
||||||
if (!_gpuCache || _gpuCache.length === 0) return [];
|
if (!_gpuCache || _gpuCache.length === 0) return [];
|
||||||
|
|
||||||
const QUERY = '--query-gpu=index,utilization.gpu,memory.used,memory.total';
|
const GPU_QUERY = '--query-gpu=index,utilization.gpu,utilization.encoder,memory.used,memory.total';
|
||||||
const FMT = '--format=csv,noheader,nounits';
|
const APP_QUERY = '--query-compute-apps=gpu_uuid,pid,used_memory';
|
||||||
|
const FMT = '--format=csv,noheader,nounits';
|
||||||
|
// Map GPU index → uuid so compute-app rows (keyed by uuid) attach to a GPU.
|
||||||
|
const UUID_QUERY = '--query-gpu=index,uuid';
|
||||||
|
const SCRIPT = [
|
||||||
|
`nvidia-smi ${GPU_QUERY} ${FMT} || true`,
|
||||||
|
`echo '---SEP-APPS---'`,
|
||||||
|
`nvidia-smi ${APP_QUERY} ${FMT} 2>/dev/null || true`,
|
||||||
|
`echo '---SEP-UUID---'`,
|
||||||
|
`nvidia-smi ${UUID_QUERY} ${FMT} 2>/dev/null || true`,
|
||||||
|
].join('; ');
|
||||||
|
|
||||||
let containerId;
|
let containerId;
|
||||||
try {
|
try {
|
||||||
const createRes = await dockerApi('POST', '/containers/create', {
|
const createRes = await dockerApi('POST', '/containers/create', {
|
||||||
Image: 'ubuntu:22.04',
|
Image: 'ubuntu:22.04',
|
||||||
Cmd: ['nvidia-smi', QUERY, FMT],
|
Cmd: ['sh', '-c', SCRIPT],
|
||||||
HostConfig: {
|
HostConfig: {
|
||||||
AutoRemove: false,
|
AutoRemove: false,
|
||||||
Runtime: 'nvidia',
|
Runtime: 'nvidia',
|
||||||
|
|
@ -295,11 +671,46 @@ async function sampleGpuUtil() {
|
||||||
});
|
});
|
||||||
|
|
||||||
const text = logRes.replace(/[\x00-\x07].{7}/g, '').trim();
|
const text = logRes.replace(/[\x00-\x07].{7}/g, '').trim();
|
||||||
const lines = text.split('\n').filter(l => /^\d+,/.test(l.trim()));
|
const [gpuBlock = '', appBlock = '', uuidBlock = ''] =
|
||||||
|
text.split(/---SEP-(?:APPS|UUID)---/);
|
||||||
|
|
||||||
|
// uuid → index map (for attributing compute-app rows to a GPU)
|
||||||
|
const uuidToIndex = {};
|
||||||
|
uuidBlock.split('\n').forEach(l => {
|
||||||
|
const m = l.trim().match(/^(\d+)\s*,\s*(GPU-[0-9a-fA-F-]+)/);
|
||||||
|
if (m) uuidToIndex[m[2]] = parseInt(m[1], 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
// NVENC/compute session count per GPU index (best-effort).
|
||||||
|
const sessionsByIndex = {};
|
||||||
|
appBlock.split('\n').forEach(l => {
|
||||||
|
const parts = l.split(',').map(s => s.trim());
|
||||||
|
const uuid = parts[0];
|
||||||
|
if (!uuid || !uuid.startsWith('GPU-')) return;
|
||||||
|
const idx = uuidToIndex[uuid];
|
||||||
|
if (idx == null) return;
|
||||||
|
sessionsByIndex[idx] = (sessionsByIndex[idx] || 0) + 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
const lines = gpuBlock.split('\n').filter(l => /^\s*\d+\s*,/.test(l));
|
||||||
|
|
||||||
return lines.map(line => {
|
return lines.map(line => {
|
||||||
const [idx, util, memUsed, memTotal] = line.split(',').map(s => parseInt(s.trim(), 10));
|
// utilization.encoder may report "[N/A]" on cards/drivers that don't
|
||||||
return { index: idx, util_pct: util, mem_used_mb: memUsed, mem_total_mb: memTotal };
|
// expose it — parseInt yields NaN there, which we coerce to null.
|
||||||
|
const cols = line.split(',').map(s => s.trim());
|
||||||
|
const idx = parseInt(cols[0], 10);
|
||||||
|
const util = parseInt(cols[1], 10);
|
||||||
|
const encUtil = parseInt(cols[2], 10);
|
||||||
|
const memUsed = parseInt(cols[3], 10);
|
||||||
|
const memTotal = parseInt(cols[4], 10);
|
||||||
|
return {
|
||||||
|
index: idx,
|
||||||
|
util_pct: Number.isNaN(util) ? null : util,
|
||||||
|
enc_util_pct: Number.isNaN(encUtil) ? null : encUtil,
|
||||||
|
mem_used_mb: Number.isNaN(memUsed) ? null : memUsed,
|
||||||
|
mem_total_mb: Number.isNaN(memTotal) ? null : memTotal,
|
||||||
|
nvenc_sessions: sessionsByIndex[idx] || 0,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('[gpu-util] sampling failed:', err.message);
|
console.warn('[gpu-util] sampling failed:', err.message);
|
||||||
|
|
@ -480,12 +891,31 @@ async function heartbeat() {
|
||||||
const ip_address = getIp();
|
const ip_address = getIp();
|
||||||
const capabilities = detectHardware();
|
const capabilities = detectHardware();
|
||||||
|
|
||||||
|
// Issue #166 — fold live NVENC/GPU encode telemetry into capabilities.gpus so
|
||||||
|
// the Cluster screen (which reads cluster_nodes.capabilities.gpus) can render
|
||||||
|
// per-GPU util / encoder util / NVENC sessions alongside the static name+VRAM.
|
||||||
|
// gpu_util is also sent verbatim below for any consumer reading metrics.gpus.
|
||||||
|
if (Array.isArray(capabilities.gpus) && gpu_util.length) {
|
||||||
|
capabilities.gpus = capabilities.gpus.map(g => {
|
||||||
|
const live = gpu_util.find(u => u.index === g.index);
|
||||||
|
if (!live) return g;
|
||||||
|
return {
|
||||||
|
...g,
|
||||||
|
util_pct: live.util_pct,
|
||||||
|
enc_util_pct: live.enc_util_pct,
|
||||||
|
mem_used_mb: live.mem_used_mb,
|
||||||
|
mem_total_mb: live.mem_total_mb ?? g.memory_mb ?? null,
|
||||||
|
nvenc_sessions: live.nvenc_sessions,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
hostname: os.hostname(),
|
hostname: NODE_NAME,
|
||||||
ip_address,
|
ip_address,
|
||||||
role: NODE_ROLE,
|
role: NODE_ROLE,
|
||||||
version: VERSION,
|
version: VERSION,
|
||||||
api_url: `http://${ip_address || os.hostname()}:${AGENT_PORT}`,
|
api_url: `http://${ip_address || NODE_NAME}:${AGENT_PORT}`,
|
||||||
cpu_usage,
|
cpu_usage,
|
||||||
mem_used_mb: Math.round((totalMem - freeMem) / 1048576),
|
mem_used_mb: Math.round((totalMem - freeMem) / 1048576),
|
||||||
mem_total_mb: Math.round(totalMem / 1048576),
|
mem_total_mb: Math.round(totalMem / 1048576),
|
||||||
|
|
@ -572,6 +1002,16 @@ const server = http.createServer((req, res) => {
|
||||||
const id = pathname.slice('/sidecar/'.length, -'/status'.length);
|
const id = pathname.slice('/sidecar/'.length, -'/status'.length);
|
||||||
handleSidecarStatus(id, res);
|
handleSidecarStatus(id, res);
|
||||||
|
|
||||||
|
} else if (req.method === 'GET' && pathname === '/driver/status') {
|
||||||
|
if (!checkAgentAuth(req)) return jsonResponse(res, 401, { error: 'Unauthorized' });
|
||||||
|
handleDriverStatus(res);
|
||||||
|
|
||||||
|
} else if (req.method === 'POST' && pathname === '/driver/install') {
|
||||||
|
if (!checkAgentAuth(req)) return jsonResponse(res, 401, { error: 'Unauthorized' });
|
||||||
|
readBody(req)
|
||||||
|
.then(body => handleDriverInstall(body, res))
|
||||||
|
.catch(() => jsonResponse(res, 400, { error: 'Invalid request body' }));
|
||||||
|
|
||||||
} else if (req.method === 'GET' && pathname.startsWith('/live/')) {
|
} else if (req.method === 'GET' && pathname.startsWith('/live/')) {
|
||||||
serveLiveFile(pathname, res);
|
serveLiveFile(pathname, res);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -78,10 +78,6 @@ RUN set -eux; \
|
||||||
echo "caspar binary: /opt/casparcg_server/bin/casparcg"; \
|
echo "caspar binary: /opt/casparcg_server/bin/casparcg"; \
|
||||||
cd /; rm -rf /tmp/caspar
|
cd /; rm -rf /tmp/caspar
|
||||||
|
|
||||||
# ── NDI runtime (optional) ───────────────────────────────────────────────────
|
|
||||||
# If an NDI SDK tarball URL is provided, extract its libs to /opt/ndi-lib and
|
|
||||||
# point CasparCG at them via NDI_RUNTIME_DIR_V6. Pin the SDK version to what the
|
|
||||||
# server expects (the common docker failure is a libndi .so version mismatch).
|
|
||||||
RUN if [ -n "$NDI_SDK_URL" ]; then \
|
RUN if [ -n "$NDI_SDK_URL" ]; then \
|
||||||
mkdir -p /opt/ndi-lib && \
|
mkdir -p /opt/ndi-lib && \
|
||||||
curl -fsSL "$NDI_SDK_URL" -o /tmp/ndi.tar.gz && \
|
curl -fsSL "$NDI_SDK_URL" -o /tmp/ndi.tar.gz && \
|
||||||
|
|
@ -91,16 +87,13 @@ RUN if [ -n "$NDI_SDK_URL" ]; then \
|
||||||
fi
|
fi
|
||||||
ENV NDI_RUNTIME_DIR_V6=/opt/ndi-lib
|
ENV NDI_RUNTIME_DIR_V6=/opt/ndi-lib
|
||||||
|
|
||||||
# CasparCG media folder — mam-api stages assets from S3 into this volume.
|
|
||||||
RUN mkdir -p /media
|
RUN mkdir -p /media
|
||||||
|
|
||||||
# ── Node control shim ────────────────────────────────────────────────────────
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
RUN npm install --omit=dev
|
RUN npm install --omit=dev
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# CasparCG config + entrypoint
|
|
||||||
COPY casparcg.config /opt/casparcg/casparcg.config
|
COPY casparcg.config /opt/casparcg/casparcg.config
|
||||||
COPY entrypoint.sh /entrypoint.sh
|
COPY entrypoint.sh /entrypoint.sh
|
||||||
RUN chmod +x /entrypoint.sh
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# Headless GL: start a virtual framebuffer unless a real DISPLAY is provided
|
|
||||||
# (a GPU node may pass through an X socket). CasparCG's mixer needs a GL context.
|
|
||||||
if [ -z "${DISPLAY:-}" ]; then
|
if [ -z "${DISPLAY:-}" ]; then
|
||||||
echo "[entrypoint] starting Xvfb on :99"
|
echo "[entrypoint] starting Xvfb on :99"
|
||||||
Xvfb :99 -screen 0 1920x1080x24 -nolisten tcp &
|
Xvfb :99 -screen 0 1920x1080x24 -nolisten tcp &
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,20 @@ app.post('/transport/skip', async (req, res) => { try { res.json(await playout
|
||||||
app.post('/transport/pause', async (req, res) => { try { res.json(await playoutManager.pause()); } catch (e) { res.status(500).json({ error: e.message }); } });
|
app.post('/transport/pause', async (req, res) => { try { res.json(await playoutManager.pause()); } catch (e) { res.status(500).json({ error: e.message }); } });
|
||||||
app.post('/transport/resume', async (req, res) => { try { res.json(await playoutManager.resume()); } catch (e) { res.status(500).json({ error: e.message }); } });
|
app.post('/transport/resume', async (req, res) => { try { res.json(await playoutManager.resume()); } catch (e) { res.status(500).json({ error: e.message }); } });
|
||||||
|
|
||||||
|
// Fire an SCTE-35 ad-break splice on the live output. Body:
|
||||||
|
// { eventId, type: 'splice_insert'|'immediate'|'splice_out'|'splice_in', durationS }
|
||||||
|
// Returns the active-break descriptor (or the splice_in ack) so the mam-api can
|
||||||
|
// stamp the as-run log.
|
||||||
|
app.post('/scte/trigger', (req, res) => {
|
||||||
|
try {
|
||||||
|
const { eventId = 1, type = 'splice_insert', durationS = 30 } = req.body || {};
|
||||||
|
res.json(playoutManager.triggerScte({ eventId, type, durationS }));
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[playout] /scte/trigger error:', err.message);
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.get('/status', (req, res) => res.json(playoutManager.getStatus()));
|
app.get('/status', (req, res) => res.json(playoutManager.getStatus()));
|
||||||
|
|
||||||
// Auto-start: when the sidecar is spawned by mam-api with channel env, bring up
|
// Auto-start: when the sidecar is spawned by mam-api with channel env, bring up
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { AmcpClient } from './amcp.js';
|
import { AmcpClient } from './amcp.js';
|
||||||
import { spawn } from 'node:child_process';
|
import { spawn } from 'node:child_process';
|
||||||
import { mkdirSync } from 'node:fs';
|
import { mkdirSync, readdirSync, unlinkSync } from 'node:fs';
|
||||||
|
|
||||||
// Playout manager — owns one CasparCG channel's lifecycle inside this sidecar.
|
// Playout manager — owns one CasparCG channel's lifecycle inside this sidecar.
|
||||||
//
|
//
|
||||||
|
|
@ -83,8 +83,13 @@ export class PlayoutManager {
|
||||||
currentClip: null,
|
currentClip: null,
|
||||||
startedAt: null,
|
startedAt: null,
|
||||||
lastError: null,
|
lastError: null,
|
||||||
|
// SCTE-35: the currently-active ad break, if any. Set by triggerScte and
|
||||||
|
// cleared by a timer when the break window elapses. Surfaced in getStatus
|
||||||
|
// so the UI can render an "in break" state + countdown.
|
||||||
|
scteActive: null, // { eventId, type, durationS, firedAt(iso), endsAt(iso) }
|
||||||
};
|
};
|
||||||
this._advanceTimer = null;
|
this._advanceTimer = null;
|
||||||
|
this._scteTimer = null;
|
||||||
this._hlsProc = null; // standalone ffmpeg re-mux child process
|
this._hlsProc = null; // standalone ffmpeg re-mux child process
|
||||||
this._hlsRestartTimer = null;
|
this._hlsRestartTimer = null;
|
||||||
}
|
}
|
||||||
|
|
@ -211,6 +216,20 @@ export class PlayoutManager {
|
||||||
_startHlsRemux() {
|
_startHlsRemux() {
|
||||||
if (!HLS_DIR) return;
|
if (!HLS_DIR) return;
|
||||||
try { mkdirSync(HLS_DIR, { recursive: true }); } catch (_) {}
|
try { mkdirSync(HLS_DIR, { recursive: true }); } catch (_) {}
|
||||||
|
// Purge stale HLS artifacts from any prior session before starting. The
|
||||||
|
// /media volume is a shared host bind, so a previous (or duplicate/failover)
|
||||||
|
// sidecar can leave orphaned index*.ts + an old index.m3u8 behind. ffmpeg's
|
||||||
|
// index%d.ts counter restarts at 0, so those leftovers collide with the new
|
||||||
|
// segment numbering and can briefly corrupt the live playlist hls.js reads
|
||||||
|
// (it sees a frozen / non-monotonic edge → monitor goes black). A clean dir
|
||||||
|
// per session guarantees a coherent live timeline.
|
||||||
|
try {
|
||||||
|
for (const f of readdirSync(HLS_DIR)) {
|
||||||
|
if (/\.ts$/.test(f) || /\.m3u8$/.test(f)) {
|
||||||
|
try { unlinkSync(`${HLS_DIR}/${f}`); } catch (_) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
this._stopHlsRemux();
|
this._stopHlsRemux();
|
||||||
|
|
||||||
const out = `${HLS_DIR}/index.m3u8`;
|
const out = `${HLS_DIR}/index.m3u8`;
|
||||||
|
|
@ -291,6 +310,7 @@ export class PlayoutManager {
|
||||||
|
|
||||||
async stopChannel() {
|
async stopChannel() {
|
||||||
this._clearAdvance();
|
this._clearAdvance();
|
||||||
|
this._clearScte();
|
||||||
this.state.running = false; // set first so the ffmpeg exit handler won't respawn
|
this.state.running = false; // set first so the ffmpeg exit handler won't respawn
|
||||||
this._stopHlsRemux();
|
this._stopHlsRemux();
|
||||||
try { await this.amcp.send(`STOP ${CHANNEL}-${FG_LAYER}`); } catch (_) {}
|
try { await this.amcp.send(`STOP ${CHANNEL}-${FG_LAYER}`); } catch (_) {}
|
||||||
|
|
@ -416,6 +436,92 @@ export class PlayoutManager {
|
||||||
return this.getStatus();
|
return this.getStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── SCTE-35 ad-break splice ──────────────────────────────────────────────
|
||||||
|
// Act on an ad-break cue. The mam-api owns scheduling + persistence; the
|
||||||
|
// sidecar performs the actual splice on the live output and tracks the active
|
||||||
|
// break locally so /status can report a countdown.
|
||||||
|
//
|
||||||
|
// What this does today, end to end:
|
||||||
|
// 1. Records the break as the active break (UI reads it from /status for the
|
||||||
|
// "SCTE BREAK" on-air state + countdown). A timer clears it after
|
||||||
|
// durationS so the UI returns to normal automatically.
|
||||||
|
// 2. Emits an operator-visible log line at the splice point.
|
||||||
|
// 3. Returns the cue descriptor so the mam-api can stamp the as-run log.
|
||||||
|
//
|
||||||
|
// ── Real in-stream SCTE-35 injection (the injection point) ─────────────────
|
||||||
|
// True SCTE-35 requires inserting a splice_info_section into the OUTPUT
|
||||||
|
// transport stream on a dedicated SCTE-35 PID, time-aligned to the splice
|
||||||
|
// point (pts_time). CasparCG 2.3's FFMPEG consumer does NOT expose an SCTE-35
|
||||||
|
// muxer option, so we cannot ask CasparCG to carry the cue. The two viable
|
||||||
|
// production paths, neither of which the current single-process CasparCG
|
||||||
|
// output supports out of the box, are:
|
||||||
|
//
|
||||||
|
// (a) ffmpeg-based output: when the primary consumer is replaced by a
|
||||||
|
// Node-spawned ffmpeg (as the HLS preview re-mux already is), mux an
|
||||||
|
// SCTE-35 data stream. ffmpeg can pass through a -map'd scte35 PID, and
|
||||||
|
// for HLS can emit #EXT-X-CUE-OUT/#EXT-X-CUE-IN (or DATERANGE) tags. The
|
||||||
|
// hook would build the splice_insert binary section here and feed it to
|
||||||
|
// that ffmpeg via a data input / sidecar packetizer.
|
||||||
|
// (b) A downstream SCTE-35 inserter (e.g. an OTT packager / encoder that
|
||||||
|
// accepts cue triggers over its own API). The hook would POST the cue
|
||||||
|
// to that device's API at the splice instant.
|
||||||
|
//
|
||||||
|
// Until one of those output paths is wired, the splice is faithfully
|
||||||
|
// scheduled, triggered, countdown-tracked, and as-run-logged — but the cue is
|
||||||
|
// NOT yet embedded in the SRT/RTMP/SDI/NDI elementary stream. Replace the body
|
||||||
|
// of _injectScteCue below to enable real injection.
|
||||||
|
triggerScte({ eventId = 1, type = 'splice_insert', durationS = 30 } = {}) {
|
||||||
|
const firedAt = new Date();
|
||||||
|
const endsAt = new Date(firedAt.getTime() + (durationS > 0 ? durationS * 1000 : 0));
|
||||||
|
|
||||||
|
// Build + emit the cue on the output (TODO injection point — see above).
|
||||||
|
this._injectScteCue({ eventId, type, durationS });
|
||||||
|
|
||||||
|
// A splice_in / return-to-network ends any active break immediately.
|
||||||
|
if (type === 'splice_in') {
|
||||||
|
this._clearScte();
|
||||||
|
console.log(`[playout][scte] splice_in event=${eventId} — return to network`);
|
||||||
|
return { eventId, type, durationS: 0, firedAt: firedAt.toISOString(), endsAt: firedAt.toISOString() };
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state.scteActive = {
|
||||||
|
eventId, type, durationS,
|
||||||
|
firedAt: firedAt.toISOString(),
|
||||||
|
endsAt: endsAt.toISOString(),
|
||||||
|
};
|
||||||
|
console.log(`[playout][scte] ${type} event=${eventId} duration=${durationS}s — splice OUT at ${firedAt.toISOString()}`);
|
||||||
|
|
||||||
|
// Auto-clear the active break when its window elapses (splice_out is
|
||||||
|
// open-ended, so it stays until an explicit splice_in).
|
||||||
|
if (this._scteTimer) { clearTimeout(this._scteTimer); this._scteTimer = null; }
|
||||||
|
if (durationS > 0 && type !== 'splice_out') {
|
||||||
|
this._scteTimer = setTimeout(() => {
|
||||||
|
this._scteTimer = null;
|
||||||
|
console.log(`[playout][scte] break event=${eventId} ended — return to network`);
|
||||||
|
this._clearScte();
|
||||||
|
}, durationS * 1000);
|
||||||
|
}
|
||||||
|
return this.state.scteActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The SCTE-35 cue packetizer / injection hook. See the long comment on
|
||||||
|
// triggerScte for why this is a stub on the current CasparCG output path and
|
||||||
|
// what to put here to enable real in-stream injection.
|
||||||
|
_injectScteCue({ eventId, type, durationS }) {
|
||||||
|
// TODO(scte-injection): build the splice_info_section (splice_insert with
|
||||||
|
// splice_event_id=eventId, out_of_network_indicator per type,
|
||||||
|
// break_duration=durationS*90000 ticks) and emit it on the output's SCTE-35
|
||||||
|
// PID via an ffmpeg-based output, or POST it to a downstream inserter's API.
|
||||||
|
// No-op until the output path supports it; the scheduling/trigger/as-run
|
||||||
|
// path above is fully functional regardless.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_clearScte() {
|
||||||
|
if (this._scteTimer) { clearTimeout(this._scteTimer); this._scteTimer = null; }
|
||||||
|
this.state.scteActive = null;
|
||||||
|
}
|
||||||
|
|
||||||
_reportAsRunStart(item) {
|
_reportAsRunStart(item) {
|
||||||
// The mam-api owns the as-run table; the sidecar just logs locally. The API
|
// The mam-api owns the as-run table; the sidecar just logs locally. The API
|
||||||
// polls /status and writes as-run rows on clip change. Keeping the DB write
|
// polls /status and writes as-run rows on clip change. Keeping the DB write
|
||||||
|
|
@ -437,6 +543,7 @@ export class PlayoutManager {
|
||||||
loop: this.state.loop,
|
loop: this.state.loop,
|
||||||
startedAt: this.state.startedAt,
|
startedAt: this.state.startedAt,
|
||||||
lastError: this.state.lastError,
|
lastError: this.state.lastError,
|
||||||
|
scteActive: this.state.scteActive || null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,18 @@ function App() {
|
||||||
try { localStorage.setItem('df.sidebar.collapsed', next ? '1' : '0'); } catch {}
|
try { localStorage.setItem('df.sidebar.collapsed', next ? '1' : '0'); } catch {}
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Sync route state with URL hash
|
||||||
|
React.useEffect(() => {
|
||||||
|
const parseHash = () => {
|
||||||
|
const hash = window.location.hash.slice(1); // remove #
|
||||||
|
const route = hash.startsWith('/') ? hash.slice(1) : hash || 'home';
|
||||||
|
setRoute(route);
|
||||||
|
};
|
||||||
|
parseHash();
|
||||||
|
window.addEventListener('hashchange', parseHash);
|
||||||
|
return () => window.removeEventListener('hashchange', parseHash);
|
||||||
|
}, []);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
|
|
||||||
|
|
@ -28,16 +28,29 @@ window.ZAMPP_API_PREFIX = API; // single source of truth (#115)
|
||||||
// The panel is now a UXP plugin (.ccx), replacing the legacy CEP/ZXP panel.
|
// The panel is now a UXP plugin (.ccx), replacing the legacy CEP/ZXP panel.
|
||||||
// Each entry carries a `ccx` URL; the older `zxp`/`installer` fields are gone.
|
// Each entry carries a `ccx` URL; the older `zxp`/`installer` fields are gone.
|
||||||
window.PREMIERE_RELEASES = [
|
window.PREMIERE_RELEASES = [
|
||||||
|
{
|
||||||
|
version: '2.2.3',
|
||||||
|
ccx: '/downloads/dragonflight-mam-2.2.3.ccx',
|
||||||
|
installer: null,
|
||||||
|
notes: 'Fix: streaming write for large imports (no more truncated files). UXP plugin: one-click Export Timeline, library + conform + auto-relink, growing-file mount.',
|
||||||
|
latest: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
version: '2.2.2',
|
version: '2.2.2',
|
||||||
ccx: '/downloads/dragonflight-mam-2.2.2.ccx',
|
ccx: '/downloads/dragonflight-mam-2.2.2.ccx',
|
||||||
installer: null,
|
installer: null,
|
||||||
notes: 'UXP plugin: one-click Export Timeline, library + conform + auto-relink, growing-file mount, runtime version chip. Replaces the legacy CEP/ZXP panel.',
|
notes: 'UXP plugin: one-click Export Timeline, library + conform + auto-relink, growing-file mount, runtime version chip. Replaces the legacy CEP/ZXP panel.',
|
||||||
latest: true,
|
latest: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
window.PREMIERE_LATEST = window.PREMIERE_RELEASES.find(r => r.latest) || window.PREMIERE_RELEASES[0];
|
window.PREMIERE_LATEST = window.PREMIERE_RELEASES.find(r => r.latest) || window.PREMIERE_RELEASES[0];
|
||||||
|
|
||||||
|
// Teams ISO workstation installer. Placeholder slot: the .exe is not in the
|
||||||
|
// repo yet, so `available` is false and the Downloads modal renders the row
|
||||||
|
// disabled with a "coming soon" note. Drop the file into public/downloads/
|
||||||
|
// and flip `available: true` (set `version`) to finish it.
|
||||||
|
window.TEAMS_ISO = { version: null, url: '/downloads/TeamsISO.exe', available: false };
|
||||||
|
|
||||||
window.ZAMPP_DATA = {
|
window.ZAMPP_DATA = {
|
||||||
PROJECTS: [],
|
PROJECTS: [],
|
||||||
ASSETS: [],
|
ASSETS: [],
|
||||||
|
|
|
||||||
BIN
services/web-ui/public/downloads/dragonflight-mam-2.2.3.ccx
Normal file
BIN
services/web-ui/public/downloads/dragonflight-mam-2.2.3.ccx
Normal file
Binary file not shown.
|
|
@ -153,7 +153,7 @@ function NewRecorderModal({ open, onClose }) {
|
||||||
const [recCodec, setRecCodec] = React.useState('hevc_nvenc');
|
const [recCodec, setRecCodec] = React.useState('hevc_nvenc');
|
||||||
// Custom target bitrate in Mbps for bitrate-controlled codecs (NVENC / x264 /
|
// Custom target bitrate in Mbps for bitrate-controlled codecs (NVENC / x264 /
|
||||||
// x265 / DNxHD). ProRes ignores bitrate (quality is profile-driven).
|
// x265 / DNxHD). ProRes ignores bitrate (quality is profile-driven).
|
||||||
const [recBitrate, setRecBitrate] = React.useState('60');
|
const [recBitrate, setRecBitrate] = React.useState('25');
|
||||||
// Container is derived from the codec, not manually chosen: HEVC/ProRes/DNxHR
|
// Container is derived from the codec, not manually chosen: HEVC/ProRes/DNxHR
|
||||||
// → MOV (fragmented, growing-capable); H.264 → MP4.
|
// → MOV (fragmented, growing-capable); H.264 → MP4.
|
||||||
const recContainer = (recCodec === 'h264' || recCodec === 'h264_nvenc' || recCodec === 'libx264') ? 'mp4' : 'mov';
|
const recContainer = (recCodec === 'h264' || recCodec === 'h264_nvenc' || recCodec === 'libx264') ? 'mp4' : 'mov';
|
||||||
|
|
@ -162,6 +162,12 @@ function NewRecorderModal({ open, onClose }) {
|
||||||
const codecUsesBitrate = BITRATE_CODECS.has(recCodec);
|
const codecUsesBitrate = BITRATE_CODECS.has(recCodec);
|
||||||
const [proxyOn, setProxyOn] = React.useState(true);
|
const [proxyOn, setProxyOn] = React.useState(true);
|
||||||
const [growingOn, setGrowingOn] = React.useState(false);
|
const [growingOn, setGrowingOn] = React.useState(false);
|
||||||
|
// Growing-files mode forces the master to XDCAM HD422 / MXF OP1a in the capture
|
||||||
|
// backend (the only growing format Premiere can import live), but the target
|
||||||
|
// bitrate is still operator-controlled and applied via -b:v. Keep the bitrate
|
||||||
|
// input visible/editable whenever growing is on, even if the selected (and
|
||||||
|
// soon-to-be-overridden) codec would normally be quality-driven (ProRes).
|
||||||
|
const showBitrate = codecUsesBitrate || growingOn;
|
||||||
const [projectId, setProjectId] = React.useState(PROJECTS[0]?.id || '');
|
const [projectId, setProjectId] = React.useState(PROJECTS[0]?.id || '');
|
||||||
const [submitting, setSubmitting] = React.useState(false);
|
const [submitting, setSubmitting] = React.useState(false);
|
||||||
const [submitErr, setSubmitErr] = React.useState(null);
|
const [submitErr, setSubmitErr] = React.useState(null);
|
||||||
|
|
@ -214,8 +220,10 @@ function NewRecorderModal({ open, onClose }) {
|
||||||
recording_framerate: '', // empty = match source
|
recording_framerate: '', // empty = match source
|
||||||
recording_resolution: 'native',
|
recording_resolution: 'native',
|
||||||
};
|
};
|
||||||
// Custom bitrate only applies to bitrate-controlled codecs (ProRes ignores it).
|
// Custom bitrate applies to bitrate-controlled codecs AND to growing-files
|
||||||
if (codecUsesBitrate && recBitrate) {
|
// mode (which forces H.264/TS in capture but still honors -b:v). ProRes
|
||||||
|
// without growing ignores bitrate, so we omit it there.
|
||||||
|
if ((codecUsesBitrate || growingOn) && recBitrate) {
|
||||||
body.recording_video_bitrate = `${recBitrate}M`;
|
body.recording_video_bitrate = `${recBitrate}M`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -224,7 +232,11 @@ function NewRecorderModal({ open, onClose }) {
|
||||||
} else if (sourceType === 'RTMP') {
|
} else if (sourceType === 'RTMP') {
|
||||||
body.source_config = { url: rtmpUrl };
|
body.source_config = { url: rtmpUrl };
|
||||||
} else if (sourceType === 'DELTACAST') {
|
} else if (sourceType === 'DELTACAST') {
|
||||||
body.source_config = {};
|
// One Deltacast board (index 0) exposes 8 channels. The picker's selected
|
||||||
|
// index IS the capture channel, so persist it as source_config.port; the
|
||||||
|
// capture sidecar maps that to the bridge's --port. device_index is kept
|
||||||
|
// for backward-compatible display/fallback.
|
||||||
|
body.source_config = { port: dcDeviceIdx };
|
||||||
body.device_index = dcDeviceIdx;
|
body.device_index = dcDeviceIdx;
|
||||||
body.node_id = dcNodeId || undefined;
|
body.node_id = dcNodeId || undefined;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -397,10 +409,34 @@ function NewRecorderModal({ open, onClose }) {
|
||||||
</div>
|
</div>
|
||||||
<div className="modal-section-body">
|
<div className="modal-section-body">
|
||||||
{recTab === 'video' && (
|
{recTab === 'video' && (
|
||||||
|
<>
|
||||||
|
{/* Codec presets — one click fills codec + bitrate with a known-good
|
||||||
|
combo that passes the server-side validateRecorderConfig guard.
|
||||||
|
Container is derived from the codec (HEVC/ProRes/DNxHR → MOV,
|
||||||
|
H.264 → MP4), and master audio is always PCM (valid in MOV). */}
|
||||||
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginBottom: 12 }}>
|
||||||
|
{[
|
||||||
|
{ id: 'hevc', label: 'HEVC Master (MOV)', codec: 'hevc_nvenc', bitrate: '25' },
|
||||||
|
{ id: 'h264', label: 'H.264 Proxy-friendly (MP4)', codec: 'h264_nvenc', bitrate: '25' },
|
||||||
|
{ id: 'dnxhr', label: 'DNxHR HQ (MOV)', codec: 'dnxhr_hq', bitrate: '145' },
|
||||||
|
].map(p => (
|
||||||
|
<button key={p.id}
|
||||||
|
className={`btn ghost sm${recCodec === p.codec ? ' active' : ''}`}
|
||||||
|
onClick={() => { setRecCodec(p.codec); setRecBitrate(p.bitrate); }}
|
||||||
|
style={{ flexShrink: 0 }}>
|
||||||
|
{p.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<label className="field-label">Video codec</label>
|
<label className="field-label">
|
||||||
<select className="field-input" value={recCodec} onChange={e => setRecCodec(e.target.value)} style={{ appearance: 'auto' }}>
|
Video codec{growingOn && <span style={{ marginLeft: 6, fontSize: 10, fontWeight: 700, letterSpacing: '0.06em', color: 'var(--warn, #d9a441)' }}>· LOCKED BY GROWING</span>}
|
||||||
|
</label>
|
||||||
|
<select className="field-input" value={growingOn ? 'h264_growing' : recCodec}
|
||||||
|
onChange={e => setRecCodec(e.target.value)} disabled={growingOn}
|
||||||
|
style={{ appearance: 'auto', opacity: growingOn ? 0.6 : 1 }}>
|
||||||
|
{growingOn && <option value="h264_growing">XDCAM HD422 (MXF OP1a) — growing</option>}
|
||||||
<option value="hevc_nvenc">All-Intra HEVC (NVENC) — GPU, growing</option>
|
<option value="hevc_nvenc">All-Intra HEVC (NVENC) — GPU, growing</option>
|
||||||
<option value="h264_nvenc">H.264 (NVENC) — GPU</option>
|
<option value="h264_nvenc">H.264 (NVENC) — GPU</option>
|
||||||
<option value="prores_hq">ProRes 422 HQ — 4:2:2 CPU</option>
|
<option value="prores_hq">ProRes 422 HQ — 4:2:2 CPU</option>
|
||||||
|
|
@ -412,7 +448,7 @@ function NewRecorderModal({ open, onClose }) {
|
||||||
<option value="libx265">H.265 (x265, CPU)</option>
|
<option value="libx265">H.265 (x265, CPU)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
{codecUsesBitrate ? (
|
{showBitrate ? (
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<label className="field-label">Target bitrate (Mbps)</label>
|
<label className="field-label">Target bitrate (Mbps)</label>
|
||||||
<input
|
<input
|
||||||
|
|
@ -444,6 +480,7 @@ function NewRecorderModal({ open, onClose }) {
|
||||||
return null;
|
return null;
|
||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{recTab === 'audio' && (
|
{recTab === 'audio' && (
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
|
||||||
|
|
@ -455,8 +492,10 @@ function NewRecorderModal({ open, onClose }) {
|
||||||
)}
|
)}
|
||||||
{recTab === 'container' && (
|
{recTab === 'container' && (
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
|
||||||
<Field label="Container" value={recContainer === 'mp4' ? 'MP4 (auto)' : 'Fragmented MOV (auto)'} select />
|
<Field label="Container"
|
||||||
<Field label="Growing-file" value={recContainer === 'mov' ? 'Supported (edit-while-record)' : 'No'} select />
|
value={growingOn ? 'MXF OP1a (growing, auto)' : (recContainer === 'mp4' ? 'MP4 (auto)' : 'Fragmented MOV (auto)')} select />
|
||||||
|
<Field label="Growing-file"
|
||||||
|
value={growingOn ? 'On (edit-while-record, locked)' : (recContainer === 'mov' ? 'Supported (edit-while-record)' : 'No')} select />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -486,6 +525,14 @@ function NewRecorderModal({ open, onClose }) {
|
||||||
Write the live master to the SMB share so editors can cut while it's still recording.
|
Write the live master to the SMB share so editors can cut while it's still recording.
|
||||||
Requires the SMB share to be configured in Settings → Storage.
|
Requires the SMB share to be configured in Settings → Storage.
|
||||||
</div>
|
</div>
|
||||||
|
{growingOn && (
|
||||||
|
<div style={{ fontSize: 11, color: 'var(--warn, #d9a441)', marginTop: 6 }}>
|
||||||
|
Growing-files mode records XDCAM HD422 (MPEG-2 4:2:2 CBR) in MXF OP1a — the format Premiere supports for edit-while-record growing files. Bitrate below still applies.
|
||||||
|
Premiere can import while it's still being written. The codec and container above
|
||||||
|
are overridden for this recorder (the target bitrate still applies). Turn growing
|
||||||
|
off to record your selected master codec/container.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,12 @@ function _normalizeNode(n, x, y) {
|
||||||
index: g.index ?? 0,
|
index: g.index ?? 0,
|
||||||
device: g.device || null,
|
device: g.device || null,
|
||||||
bound: !!(g.name && g.memory_mb), // name+memory = nvidia-smi confirmed driver bound
|
bound: !!(g.name && g.memory_mb), // name+memory = nvidia-smi confirmed driver bound
|
||||||
|
// Issue #166 — live NVENC/GPU encode telemetry folded into capabilities.gpus
|
||||||
|
// by the node-agent heartbeat (null until a heartbeat carries it / a GPU node).
|
||||||
|
utilPct: g.util_pct != null ? g.util_pct : null,
|
||||||
|
encUtilPct: g.enc_util_pct != null ? g.enc_util_pct : null,
|
||||||
|
memUsedMb: g.mem_used_mb != null ? g.mem_used_mb : null,
|
||||||
|
nvencSessions: g.nvenc_sessions != null ? g.nvenc_sessions : null,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Blackmagic DeckLink: capabilities.blackmagic + capabilities.blackmagic_model
|
// Blackmagic DeckLink: capabilities.blackmagic + capabilities.blackmagic_model
|
||||||
|
|
@ -21,6 +27,13 @@ function _normalizeNode(n, x, y) {
|
||||||
online: b.online !== false,
|
online: b.online !== false,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Deltacast ports — used by the Capture Drivers panel as a secondary
|
||||||
|
// "driver present?" signal (heartbeat only reports a port if the card is seen).
|
||||||
|
const deltacastPorts = (cap.deltacast || []).map(d => ({
|
||||||
|
index: d.index ?? 0,
|
||||||
|
device: d.device || null,
|
||||||
|
}));
|
||||||
|
|
||||||
const memUsedMb = n.mem_used_mb || n.memory_used_mb || (n.mem && n.mem < 1000 ? n.mem * 1024 : n.mem || 0);
|
const memUsedMb = n.mem_used_mb || n.memory_used_mb || (n.mem && n.mem < 1000 ? n.mem * 1024 : n.mem || 0);
|
||||||
const memTotalMb = n.mem_total_mb || n.memory_total_mb || (n.memTotal && n.memTotal < 1000 ? n.memTotal * 1024 : n.memTotal || 0);
|
const memTotalMb = n.mem_total_mb || n.memory_total_mb || (n.memTotal && n.memTotal < 1000 ? n.memTotal * 1024 : n.memTotal || 0);
|
||||||
|
|
||||||
|
|
@ -38,6 +51,7 @@ function _normalizeNode(n, x, y) {
|
||||||
// Raw capabilities for the hardware panel
|
// Raw capabilities for the hardware panel
|
||||||
gpus,
|
gpus,
|
||||||
bmdPorts,
|
bmdPorts,
|
||||||
|
deltacastPorts,
|
||||||
// Legacy flat arrays kept for the stat-row summary cards
|
// Legacy flat arrays kept for the stat-row summary cards
|
||||||
gpuCount: gpus.length,
|
gpuCount: gpus.length,
|
||||||
bmdCount: bmdPorts.length,
|
bmdCount: bmdPorts.length,
|
||||||
|
|
@ -116,6 +130,7 @@ function Users() {
|
||||||
const [editingUser, setEditingUser] = React.useState(null);
|
const [editingUser, setEditingUser] = React.useState(null);
|
||||||
const [resetUser, setResetUser] = React.useState(null);
|
const [resetUser, setResetUser] = React.useState(null);
|
||||||
const [menuFor, setMenuFor] = React.useState(null); // row id whose menu is open
|
const [menuFor, setMenuFor] = React.useState(null); // row id whose menu is open
|
||||||
|
const [confirm, confirmModal] = window.useConfirm();
|
||||||
|
|
||||||
const refreshUsers = React.useCallback(() => {
|
const refreshUsers = React.useCallback(() => {
|
||||||
window.ZAMPP_API.fetch('/users')
|
window.ZAMPP_API.fetch('/users')
|
||||||
|
|
@ -161,9 +176,9 @@ function Users() {
|
||||||
|
|
||||||
const onCreated = () => { refreshUsers(); setShowInvite(false); };
|
const onCreated = () => { refreshUsers(); setShowInvite(false); };
|
||||||
|
|
||||||
const deleteUser = (u) => {
|
const deleteUser = async (u) => {
|
||||||
setMenuFor(null);
|
setMenuFor(null);
|
||||||
if (!confirm(`Delete user "${u.name}" (@${u.username})?\nThis cannot be undone.`)) return;
|
if (!(await confirm({ title: 'Delete user?', message: `Delete user "${u.name}" (@${u.username})?\nThis cannot be undone.` }))) return;
|
||||||
window.ZAMPP_API.fetch('/users/' + u.id, { method: 'DELETE' })
|
window.ZAMPP_API.fetch('/users/' + u.id, { method: 'DELETE' })
|
||||||
.then(refreshUsers)
|
.then(refreshUsers)
|
||||||
.catch(e => alert('Delete failed: ' + e.message));
|
.catch(e => alert('Delete failed: ' + e.message));
|
||||||
|
|
@ -180,6 +195,7 @@ function Users() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page">
|
<div className="page">
|
||||||
|
{confirmModal}
|
||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
<h1>Users & Groups</h1>
|
<h1>Users & Groups</h1>
|
||||||
<div className="spacer" />
|
<div className="spacer" />
|
||||||
|
|
@ -258,28 +274,7 @@ function Users() {
|
||||||
{tab === 'groups' && <GroupsPanel groups={groups} users={users} onChange={refreshGroups} />}
|
{tab === 'groups' && <GroupsPanel groups={groups} users={users} onChange={refreshGroups} />}
|
||||||
|
|
||||||
{tab === 'policies' && (
|
{tab === 'policies' && (
|
||||||
<div className="panel" style={{ padding: '32px 24px', color: 'var(--text-2)' }}>
|
<PoliciesPanel users={users} onChange={refreshUsers} />
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
|
|
||||||
<Icon name="lock" size={16} />
|
|
||||||
<div style={{ fontWeight: 600, fontSize: 14 }}>Access model</div>
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: 12.5, color: 'var(--text-3)', lineHeight: 1.7, maxWidth: 640 }}>
|
|
||||||
<div style={{ marginBottom: 8 }}>
|
|
||||||
<strong style={{ color: 'var(--text-2)' }}>admin</strong> — full access to every
|
|
||||||
project plus user, group, cluster, and system administration.
|
|
||||||
</div>
|
|
||||||
<div style={{ marginBottom: 8 }}>
|
|
||||||
<strong style={{ color: 'var(--text-2)' }}>editor / viewer</strong> — see only the
|
|
||||||
projects they've been granted. A <em>view</em> grant is read-only; an
|
|
||||||
<em> edit</em> grant allows changes. Grants can target an individual user or a group.
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
Manage a project's grants from the <strong style={{ color: 'var(--text-2)' }}>Projects</strong> page
|
|
||||||
→ a project's <em>Manage access…</em> menu. Group membership is managed on the
|
|
||||||
Groups tab above.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{showInvite && <InviteUserModal onCreated={onCreated} onClose={() => setShowInvite(false)} />}
|
{showInvite && <InviteUserModal onCreated={onCreated} onClose={() => setShowInvite(false)} />}
|
||||||
|
|
@ -299,6 +294,206 @@ function Users() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
// PoliciesPanel - interactive per-user permission matrix for the Policies tab.
|
||||||
|
// Keeps the access-model explainer as a small header, then renders one row per
|
||||||
|
// user with: inline role <select> (PATCH /users/:id), a 2FA badge driven by
|
||||||
|
// totp_enabled, an admin-only "Reset 2FA" action (POST /users/:id/totp/disable,
|
||||||
|
// 204), and an Access expander backed by GET /users/:id/access.
|
||||||
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
function PoliciesPanel({ users, onChange }) {
|
||||||
|
const [expandedId, setExpandedId] = React.useState(null);
|
||||||
|
const [err, setErr] = React.useState(null);
|
||||||
|
const [confirm, confirmModal] = window.useConfirm();
|
||||||
|
|
||||||
|
const changeRole = (u, newRole) => {
|
||||||
|
if (u.role === newRole) return;
|
||||||
|
setErr(null);
|
||||||
|
window.ZAMPP_API.fetch('/users/' + u.id, { method: 'PATCH', body: JSON.stringify({ role: newRole }) })
|
||||||
|
.then(() => onChange && onChange())
|
||||||
|
.catch(e => setErr('Role change failed: ' + (e.message || e)));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reset 2FA uses a raw fetch because ZAMPP_API.fetch throws on the 204 (no JSON
|
||||||
|
// body). Mirrors the disable() pattern in TotpSection.
|
||||||
|
const resetTotp = async (u) => {
|
||||||
|
if (!(await confirm({ title: 'Reset two-factor?', message: `Reset two-factor for "${u.name}" (@${u.username})?\nThey will be able to sign in without a code until they re-enrol.`, confirmLabel: 'Reset 2FA' }))) return;
|
||||||
|
setErr(null);
|
||||||
|
fetch('/api/v1/users/' + u.id + '/totp/disable', {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: { 'X-Requested-With': 'dragonflight-ui' },
|
||||||
|
})
|
||||||
|
.then(r => {
|
||||||
|
if (r.status === 204) { onChange && onChange(); return; }
|
||||||
|
return r.json().catch(() => ({})).then(b => { throw new Error(b.error || ('Failed (' + r.status + ')')); });
|
||||||
|
})
|
||||||
|
.catch(e => setErr('Reset 2FA failed: ' + (e.message || e)));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{confirmModal}
|
||||||
|
{/* Access-model explainer (kept from the old static tab, condensed) */}
|
||||||
|
<div className="panel" style={{ padding: '16px 20px', marginBottom: 12, color: 'var(--text-2)' }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
|
||||||
|
<Icon name="lock" size={15} />
|
||||||
|
<div style={{ fontWeight: 600, fontSize: 13.5 }}>Access model</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 12, color: 'var(--text-3)', lineHeight: 1.6, maxWidth: 720 }}>
|
||||||
|
<strong style={{ color: 'var(--text-2)' }}>admin</strong> has full access to every project plus
|
||||||
|
user, group, cluster, and system administration. <strong style={{ color: 'var(--text-2)' }}>editor / viewer</strong> see
|
||||||
|
only the projects they're granted — a <em>view</em> grant is read-only, an <em>edit</em> grant
|
||||||
|
allows changes, and grants can target a user or a group. Edit per-project grants from the{' '}
|
||||||
|
<a href="#" onClick={e => { e.preventDefault(); window.dispatchEvent(new CustomEvent('df:nav', { detail: 'projects' })); }}
|
||||||
|
style={{ color: 'var(--accent-text)' }}>Projects</a> page; manage group membership on the Groups tab above.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{err && <div style={{ fontSize: 12, color: 'var(--danger)', marginBottom: 8 }}>{err}</div>}
|
||||||
|
|
||||||
|
<div className="panel">
|
||||||
|
<div className="user-row head">
|
||||||
|
<div>User</div>
|
||||||
|
<div>Role</div>
|
||||||
|
<div>2FA</div>
|
||||||
|
<div>Access</div>
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
{users.length === 0 && (
|
||||||
|
<div style={{ padding: '32px 0', textAlign: 'center', color: 'var(--text-3)' }}>No users found</div>
|
||||||
|
)}
|
||||||
|
{users.map(u => (
|
||||||
|
<UserPolicyRow key={u.id} user={u}
|
||||||
|
expanded={expandedId === u.id}
|
||||||
|
onToggle={() => setExpandedId(expandedId === u.id ? null : u.id)}
|
||||||
|
onChangeRole={changeRole}
|
||||||
|
onResetTotp={resetTotp} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function UserPolicyRow({ user: u, expanded, onToggle, onChangeRole, onResetTotp }) {
|
||||||
|
const [access, setAccess] = React.useState(null); // null = not loaded, {} once fetched
|
||||||
|
const [loading, setLoading] = React.useState(false);
|
||||||
|
const [accessErr, setAccessErr] = React.useState(null);
|
||||||
|
|
||||||
|
// Lazily fetch GET /users/:id/access the first time the row is expanded.
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!expanded || access !== null) return;
|
||||||
|
setLoading(true); setAccessErr(null);
|
||||||
|
window.ZAMPP_API.fetch('/users/' + u.id + '/access')
|
||||||
|
.then(d => setAccess(d || {}))
|
||||||
|
.catch(e => { setAccess({}); setAccessErr(e.message || 'Failed to load access'); })
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}, [expanded, access, u.id]);
|
||||||
|
|
||||||
|
const projects = (access && access.projects) || [];
|
||||||
|
const memberships = (access && (access.groups || access.memberships)) || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ borderBottom: '1px solid var(--border)' }}>
|
||||||
|
<div className="user-row" style={{ borderBottom: 'none' }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||||
|
<div className="avatar" style={{ width: 32, height: 32, fontSize: 11, background: avatarColor(u.initials || u.id) }}>{u.initials || '??'}</div>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontWeight: 500, fontSize: 13 }}>{u.name}</div>
|
||||||
|
<div className="mono" style={{ fontSize: 11, color: 'var(--text-3)' }}>@{u.username}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<select value={u.role || 'viewer'}
|
||||||
|
onChange={e => onChangeRole(u, e.target.value)}
|
||||||
|
className="field-input"
|
||||||
|
style={{ width: 90, padding: '3px 6px', fontSize: 11.5, appearance: 'auto' }}>
|
||||||
|
<option value="admin">admin</option>
|
||||||
|
<option value="editor">editor</option>
|
||||||
|
<option value="viewer">viewer</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{u.totp_enabled
|
||||||
|
? <span className="badge success"><Icon name="key" size={10} /> 2FA on</span>
|
||||||
|
: <span className="badge neutral">2FA off</span>}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button className="btn ghost sm" onClick={onToggle}>
|
||||||
|
{expanded ? 'Hide' : 'View'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||||
|
{u.totp_enabled && (
|
||||||
|
<button className="btn ghost sm danger" onClick={() => onResetTotp(u)} title="Disable this user's two-factor">
|
||||||
|
<Icon name="key" size={11} />Reset 2FA
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{expanded && (
|
||||||
|
<div style={{ padding: '0 16px 16px 16px', background: 'var(--bg-2)' }}>
|
||||||
|
{loading && <div style={{ padding: '12px 0', fontSize: 12.5, color: 'var(--text-3)' }}>Loading access…</div>}
|
||||||
|
{accessErr && <div style={{ padding: '8px 0', fontSize: 12, color: 'var(--danger)' }}>{accessErr}</div>}
|
||||||
|
{!loading && !accessErr && (u.role === 'admin') && (
|
||||||
|
<div style={{ padding: '12px 0', fontSize: 12.5, color: 'var(--text-3)', display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
|
<Icon name="check" size={12} style={{ color: 'var(--success)' }} />
|
||||||
|
Admin — full access to every project.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!loading && !accessErr && u.role !== 'admin' && (
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, paddingTop: 12 }}>
|
||||||
|
{/* Accessible projects */}
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: 11, color: 'var(--text-3)', textTransform: 'uppercase', letterSpacing: '0.06em', fontWeight: 600, marginBottom: 8 }}>
|
||||||
|
Projects ({projects.length})
|
||||||
|
</div>
|
||||||
|
{projects.length === 0 && (
|
||||||
|
<div style={{ fontSize: 12, color: 'var(--text-4)' }}>No project access granted.</div>
|
||||||
|
)}
|
||||||
|
{projects.map(p => {
|
||||||
|
// Backend `via` is 'direct' for a user grant, or 'group:<name>'
|
||||||
|
// when inherited from a group. Split the label off the prefix.
|
||||||
|
const via = p.via || 'direct';
|
||||||
|
const isGroup = via.indexOf('group') === 0;
|
||||||
|
const viaLabel = isGroup ? (via.indexOf(':') >= 0 ? via.slice(via.indexOf(':') + 1) : 'group') : 'direct';
|
||||||
|
return (
|
||||||
|
<div key={(p.project_id || p.id) + ':' + via}
|
||||||
|
style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '6px 0', borderBottom: '1px solid var(--border)' }}>
|
||||||
|
<span style={{ fontSize: 12.5, flex: 1 }}>{p.project_name || p.name || p.project_id || p.id}</span>
|
||||||
|
<span className={`badge ${(p.level === 'edit') ? 'accent' : 'neutral'}`}>{p.level || 'view'}</span>
|
||||||
|
<span className="badge neutral" title={isGroup ? 'Inherited from group ' + viaLabel : 'Granted directly'}>
|
||||||
|
<Icon name={isGroup ? 'users' : 'user'} size={9} /> {viaLabel}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{/* Group memberships */}
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: 11, color: 'var(--text-3)', textTransform: 'uppercase', letterSpacing: '0.06em', fontWeight: 600, marginBottom: 8 }}>
|
||||||
|
Groups ({memberships.length})
|
||||||
|
</div>
|
||||||
|
{memberships.length === 0 && (
|
||||||
|
<div style={{ fontSize: 12, color: 'var(--text-4)' }}>Not a member of any group.</div>
|
||||||
|
)}
|
||||||
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
|
||||||
|
{memberships.map(g => (
|
||||||
|
<span key={g.id || g.group_id || g.name} className="badge neutral" style={{ display: 'inline-flex', alignItems: 'center', gap: 5 }}>
|
||||||
|
<Icon name="users" size={9} />{g.name || g.group_name || g.group_id}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function EditUserModal({ user, onClose, onSaved }) {
|
function EditUserModal({ user, onClose, onSaved }) {
|
||||||
const [name, setName] = React.useState(user.display_name || user.name || '');
|
const [name, setName] = React.useState(user.display_name || user.name || '');
|
||||||
const [saving, setSaving] = React.useState(false);
|
const [saving, setSaving] = React.useState(false);
|
||||||
|
|
@ -424,6 +619,7 @@ function GroupsPanel({ groups, users, onChange }) {
|
||||||
const [newDesc, setNewDesc] = React.useState('');
|
const [newDesc, setNewDesc] = React.useState('');
|
||||||
const [expandedId, setExpandedId] = React.useState(null);
|
const [expandedId, setExpandedId] = React.useState(null);
|
||||||
const [members, setMembers] = React.useState({}); // groupId -> [user]
|
const [members, setMembers] = React.useState({}); // groupId -> [user]
|
||||||
|
const [confirm, confirmModal] = window.useConfirm();
|
||||||
|
|
||||||
const createGroup = () => {
|
const createGroup = () => {
|
||||||
if (!newName.trim()) return;
|
if (!newName.trim()) return;
|
||||||
|
|
@ -432,8 +628,8 @@ function GroupsPanel({ groups, users, onChange }) {
|
||||||
.catch(e => alert('Create failed: ' + e.message));
|
.catch(e => alert('Create failed: ' + e.message));
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteGroup = (g) => {
|
const deleteGroup = async (g) => {
|
||||||
if (!confirm(`Delete group "${g.name}"?`)) return;
|
if (!(await confirm({ title: 'Delete group?', message: `Delete group "${g.name}"?` }))) return;
|
||||||
window.ZAMPP_API.fetch('/groups/' + g.id, { method: 'DELETE' })
|
window.ZAMPP_API.fetch('/groups/' + g.id, { method: 'DELETE' })
|
||||||
.then(onChange)
|
.then(onChange)
|
||||||
.catch(e => alert('Delete failed: ' + e.message));
|
.catch(e => alert('Delete failed: ' + e.message));
|
||||||
|
|
@ -468,6 +664,7 @@ function GroupsPanel({ groups, users, onChange }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
{confirmModal}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', marginBottom: 12 }}>
|
<div style={{ display: 'flex', alignItems: 'center', marginBottom: 12 }}>
|
||||||
<div style={{ flex: 1, fontSize: 12, color: 'var(--text-3)' }}>
|
<div style={{ flex: 1, fontSize: 12, color: 'var(--text-3)' }}>
|
||||||
Groups let you bundle users for project access. Memberships are checked when role-based access is enforced.
|
Groups let you bundle users for project access. Memberships are checked when role-based access is enforced.
|
||||||
|
|
@ -830,6 +1027,7 @@ function Containers() {
|
||||||
const [containers, setContainers] = React.useState(null);
|
const [containers, setContainers] = React.useState(null);
|
||||||
const [restartFlashState, setRestartFlashState] = React.useState(null);
|
const [restartFlashState, setRestartFlashState] = React.useState(null);
|
||||||
const [logsModalState, setLogsModalState] = React.useState(null);
|
const [logsModalState, setLogsModalState] = React.useState(null);
|
||||||
|
const [confirm, confirmModal] = window.useConfirm();
|
||||||
// #111 - guard restart-flash timers against unmount.
|
// #111 - guard restart-flash timers against unmount.
|
||||||
const mountedRef = React.useRef(true);
|
const mountedRef = React.useRef(true);
|
||||||
const flashTimerRef = React.useRef(null);
|
const flashTimerRef = React.useRef(null);
|
||||||
|
|
@ -860,8 +1058,8 @@ function Containers() {
|
||||||
|
|
||||||
const showLogs = (c) => setLogsModal(c);
|
const showLogs = (c) => setLogsModal(c);
|
||||||
|
|
||||||
const restartContainer = (c) => {
|
const restartContainer = async (c) => {
|
||||||
if (!window.confirm('Restart container "' + c.name + '"?\nIn-flight requests will be dropped.')) return;
|
if (!(await confirm({ title: 'Restart container?', message: 'Restart container "' + c.name + '"?\nIn-flight requests will be dropped.', confirmLabel: 'Restart' }))) return;
|
||||||
setRestartFlashSafe({ name: c.name, status: 'pending' });
|
setRestartFlashSafe({ name: c.name, status: 'pending' });
|
||||||
window.ZAMPP_API.fetch('/cluster/containers/' + encodeURIComponent(c.id || c.name) + '/restart', { method: 'POST' })
|
window.ZAMPP_API.fetch('/cluster/containers/' + encodeURIComponent(c.id || c.name) + '/restart', { method: 'POST' })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
|
@ -878,6 +1076,7 @@ function Containers() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page">
|
<div className="page">
|
||||||
|
{confirmModal}
|
||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
<h1>Containers</h1>
|
<h1>Containers</h1>
|
||||||
<span className="subtitle">Docker Compose services across the cluster</span>
|
<span className="subtitle">Docker Compose services across the cluster</span>
|
||||||
|
|
@ -976,7 +1175,11 @@ function Containers() {
|
||||||
<span>{(c.cpu || 0).toFixed(1)}%</span>
|
<span>{(c.cpu || 0).toFixed(1)}%</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mono" style={{ fontSize: 11.5 }}>{c.mem} MB</div>
|
<div className="mono" style={{ fontSize: 11.5 }}>
|
||||||
|
{c.memBytes != null
|
||||||
|
? `${Math.round(c.memBytes / 1048576)} MB`
|
||||||
|
: "N/A"}
|
||||||
|
</div>
|
||||||
<div className="mono" style={{ fontSize: 10.5, color: "var(--text-3)" }}>{c.ports}</div>
|
<div className="mono" style={{ fontSize: 10.5, color: "var(--text-3)" }}>{c.ports}</div>
|
||||||
<div style={{ display: "flex", gap: 4 }}>
|
<div style={{ display: "flex", gap: 4 }}>
|
||||||
<button className="btn ghost sm" onClick={() => showLogs(c)}>Logs</button>
|
<button className="btn ghost sm" onClick={() => showLogs(c)}>Logs</button>
|
||||||
|
|
@ -992,6 +1195,148 @@ function Containers() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
// DriverPanel - "Capture Drivers / SDKs" section inside the node detail panel.
|
||||||
|
// Per vendor (Blackmagic / AJA / Deltacast / NDI): shows detected status from
|
||||||
|
// GET /cluster/:id/driver-status (host probe) cross-checked with heartbeat
|
||||||
|
// capabilities, plus an "Install / Update" button that POSTs
|
||||||
|
// /cluster/:id/install-driver {vendor} and streams the agent log into a live
|
||||||
|
// output area, surfacing success/failure + "reboot required".
|
||||||
|
const DRIVER_VENDORS = [
|
||||||
|
{ key: 'blackmagic', label: 'Blackmagic', hint: 'Desktop Video driver (.deb)' },
|
||||||
|
{ key: 'aja', label: 'AJA', hint: 'NTV2 driver / SDK' },
|
||||||
|
{ key: 'deltacast', label: 'Deltacast', hint: 'VideoMaster installer' },
|
||||||
|
{ key: 'ndi', label: 'NDI', hint: 'Redistributable runtime libs' },
|
||||||
|
];
|
||||||
|
|
||||||
|
function DriverPanel({ sel }) {
|
||||||
|
const [status, setStatus] = React.useState(null); // { kernel, vendors:{...} }
|
||||||
|
const [loading, setLoading] = React.useState(true);
|
||||||
|
const [statusErr, setStatusErr] = React.useState(null);
|
||||||
|
const [busy, setBusy] = React.useState(null); // vendor key currently installing
|
||||||
|
const [log, setLog] = React.useState(null); // { vendor, text, ok, rebootRequired }
|
||||||
|
|
||||||
|
const loadStatus = React.useCallback(() => {
|
||||||
|
if (!sel.dbId) return;
|
||||||
|
setLoading(true); setStatusErr(null);
|
||||||
|
window.ZAMPP_API.fetch(`/cluster/${sel.dbId}/driver-status`)
|
||||||
|
.then(d => { setStatus(d); setLoading(false); })
|
||||||
|
.catch(e => { setStatusErr(e.message || 'unreachable'); setLoading(false); });
|
||||||
|
}, [sel.dbId]);
|
||||||
|
|
||||||
|
React.useEffect(() => { loadStatus(); }, [loadStatus]);
|
||||||
|
|
||||||
|
// Heartbeat-reported capabilities give a second signal for the two card types
|
||||||
|
// the cluster already enumerates (Blackmagic ports, Deltacast ports).
|
||||||
|
const capPresent = (vendor) => {
|
||||||
|
if (vendor === 'blackmagic') return (sel.bmdPorts || []).length > 0;
|
||||||
|
if (vendor === 'deltacast') return (sel.deltacastPorts || []).length > 0;
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isInstalled = (vendor) => {
|
||||||
|
const v = status && status.vendors && status.vendors[vendor];
|
||||||
|
return (v && v.installed) || capPresent(vendor);
|
||||||
|
};
|
||||||
|
|
||||||
|
const install = (vendor) => {
|
||||||
|
setBusy(vendor);
|
||||||
|
setLog({ vendor, text: `[ui] requesting install of ${vendor} on ${sel.id}…\n`, ok: null, rebootRequired: false });
|
||||||
|
// Raw fetch: we need the JSON body (logs) even on a non-2xx response.
|
||||||
|
fetch(`/api/v1/cluster/${sel.dbId}/install-driver`, {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'dragonflight-ui' },
|
||||||
|
body: JSON.stringify({ vendor }),
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
let body = {};
|
||||||
|
try { body = await res.json(); } catch (_) {}
|
||||||
|
const text = (body.logs && body.logs.trim())
|
||||||
|
? body.logs
|
||||||
|
: (body.error || `Install ${res.ok ? 'completed' : 'failed'} (HTTP ${res.status})`);
|
||||||
|
setLog({ vendor, text, ok: !!body.ok, rebootRequired: !!body.rebootRequired });
|
||||||
|
setBusy(null);
|
||||||
|
loadStatus();
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
setLog({ vendor, text: `[ui] request failed: ${e.message}`, ok: false, rebootRequired: false });
|
||||||
|
setBusy(null);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ marginTop: 10 }}>
|
||||||
|
<div style={{ fontSize: 11, color: "var(--text-3)", marginBottom: 6, display: "flex", alignItems: "center", gap: 6 }}>
|
||||||
|
<Icon name="download" size={11} />
|
||||||
|
Capture Drivers / SDKs
|
||||||
|
{status && status.kernel && (
|
||||||
|
<span style={{ marginLeft: "auto", fontSize: 10, color: "var(--text-4)", fontFamily: "var(--font-mono)" }}>
|
||||||
|
kernel {status.kernel}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{statusErr && (
|
||||||
|
<div style={{ fontSize: 11, color: "var(--danger)", marginBottom: 6 }}>
|
||||||
|
Driver status unavailable: {statusErr}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
|
||||||
|
{DRIVER_VENDORS.map(v => {
|
||||||
|
const installed = isInstalled(v.key);
|
||||||
|
const installing = busy === v.key;
|
||||||
|
return (
|
||||||
|
<div key={v.key} style={{
|
||||||
|
display: "flex", alignItems: "center", gap: 8,
|
||||||
|
padding: "6px 10px", background: "var(--bg-2)", borderRadius: 5,
|
||||||
|
border: "1px solid var(--border)",
|
||||||
|
}}>
|
||||||
|
<div style={{ minWidth: 0 }}>
|
||||||
|
<div style={{ fontSize: 12, fontWeight: 600, color: "var(--text-1)" }}>{v.label}</div>
|
||||||
|
<div style={{ fontSize: 10.5, color: "var(--text-4)" }}>{v.hint}</div>
|
||||||
|
</div>
|
||||||
|
<span style={{
|
||||||
|
marginLeft: "auto", fontSize: 10, fontWeight: 600, padding: "2px 6px", borderRadius: 3,
|
||||||
|
background: installed ? "rgba(91,250,138,0.15)" : "rgba(255,255,255,0.05)",
|
||||||
|
color: installed ? "var(--success)" : "var(--text-3)",
|
||||||
|
}}>
|
||||||
|
{loading ? "…" : (installed ? "INSTALLED" : "NOT INSTALLED")}
|
||||||
|
</span>
|
||||||
|
<button className="btn ghost sm" disabled={installing || !!busy}
|
||||||
|
onClick={() => install(v.key)}>
|
||||||
|
{installing ? "Installing…" : (installed ? "Update" : "Install")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{log && (
|
||||||
|
<div style={{ marginTop: 8 }}>
|
||||||
|
<div style={{ fontSize: 10.5, marginBottom: 4, display: "flex", alignItems: "center", gap: 6 }}>
|
||||||
|
<span style={{ color: "var(--text-3)" }}>{log.vendor} install output</span>
|
||||||
|
{log.ok === true && <span style={{ color: "var(--success)", fontWeight: 700 }}>SUCCESS</span>}
|
||||||
|
{log.ok === false && <span style={{ color: "var(--danger)", fontWeight: 700 }}>FAILED</span>}
|
||||||
|
{log.rebootRequired && (
|
||||||
|
<span style={{ marginLeft: "auto", fontSize: 10, fontWeight: 700, color: "var(--warning, #f0a500)" }}>
|
||||||
|
⚠ REBOOT REQUIRED
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<pre style={{
|
||||||
|
margin: 0, maxHeight: 180, overflow: "auto",
|
||||||
|
fontSize: 10.5, lineHeight: 1.4, fontFamily: "var(--font-mono)",
|
||||||
|
color: "var(--text-2)", background: "var(--bg-1)",
|
||||||
|
border: "1px solid var(--border)", borderRadius: 4, padding: "6px 8px",
|
||||||
|
whiteSpace: "pre-wrap", wordBreak: "break-word",
|
||||||
|
}}>{log.text}</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// BmdCardPanel - capture-card section inside the Cluster node detail panel.
|
// BmdCardPanel - capture-card section inside the Cluster node detail panel.
|
||||||
// Shows port chips with live video-presence dots AND the BMD SVG card diagram.
|
// Shows port chips with live video-presence dots AND the BMD SVG card diagram.
|
||||||
// ────────────────────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -1111,6 +1456,7 @@ function Cluster() {
|
||||||
const [hovered, setHovered] = React.useState(null);
|
const [hovered, setHovered] = React.useState(null);
|
||||||
// Map of "node_id:portIndex" → signal entry from /cluster/devices/blackmagic/signal
|
// Map of "node_id:portIndex" → signal entry from /cluster/devices/blackmagic/signal
|
||||||
const [portSignals, setPortSignals] = React.useState({});
|
const [portSignals, setPortSignals] = React.useState({});
|
||||||
|
const [confirm, confirmModal] = window.useConfirm();
|
||||||
|
|
||||||
const refresh = React.useCallback(() => {
|
const refresh = React.useCallback(() => {
|
||||||
window.ZAMPP_API.fetch('/cluster')
|
window.ZAMPP_API.fetch('/cluster')
|
||||||
|
|
@ -1195,8 +1541,8 @@ function Cluster() {
|
||||||
commands: [`ssh ${node.ip || node.id} 'docker stop node-agent'`],
|
commands: [`ssh ${node.ip || node.id} 'docker stop node-agent'`],
|
||||||
});
|
});
|
||||||
|
|
||||||
const removeNode = (node) => {
|
const removeNode = async (node) => {
|
||||||
if (!window.confirm('Remove node ' + node.id + ' from the cluster?\nThis does not stop the machine: it only removes it from cluster membership.')) return;
|
if (!(await confirm({ title: 'Remove node?', message: 'Remove node ' + node.id + ' from the cluster?\nThis does not stop the machine: it only removes it from cluster membership.', confirmLabel: 'Remove' }))) return;
|
||||||
window.ZAMPP_API.fetch('/cluster/nodes/' + encodeURIComponent(node.dbId || node.id), { method: 'DELETE' })
|
window.ZAMPP_API.fetch('/cluster/nodes/' + encodeURIComponent(node.dbId || node.id), { method: 'DELETE' })
|
||||||
.then(() => refresh())
|
.then(() => refresh())
|
||||||
.catch(e => setAdviceModal({ title: 'Remove failed', lines: [e.message] }));
|
.catch(e => setAdviceModal({ title: 'Remove failed', lines: [e.message] }));
|
||||||
|
|
@ -1210,6 +1556,7 @@ function Cluster() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page">
|
<div className="page">
|
||||||
|
{confirmModal}
|
||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
<h1>Cluster</h1>
|
<h1>Cluster</h1>
|
||||||
<span className="subtitle">{NODES.filter(n => n.status === "online").length} of {NODES.length} nodes online</span>
|
<span className="subtitle">{NODES.filter(n => n.status === "online").length} of {NODES.length} nodes online</span>
|
||||||
|
|
@ -1365,6 +1712,23 @@ function Cluster() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{g.device && <div style={{ fontSize: 10.5, color: "var(--text-4)", fontFamily: "var(--font-mono)" }}>{g.device}</div>}
|
{g.device && <div style={{ fontSize: 10.5, color: "var(--text-4)", fontFamily: "var(--font-mono)" }}>{g.device}</div>}
|
||||||
|
{/* Issue #166 — live NVENC/GPU encode telemetry (0 until a live encode runs) */}
|
||||||
|
{(g.utilPct != null || g.encUtilPct != null || g.nvencSessions != null) && (
|
||||||
|
<div style={{ display: "flex", flexWrap: "wrap", gap: "4px 10px", marginTop: 4, fontSize: 10.5, fontFamily: "var(--font-mono)" }}>
|
||||||
|
{g.utilPct != null && (
|
||||||
|
<span style={{ color: "var(--text-3)" }}>GPU <strong style={{ color: "var(--text-2)" }}>{g.utilPct}%</strong></span>
|
||||||
|
)}
|
||||||
|
{g.encUtilPct != null && (
|
||||||
|
<span style={{ color: "var(--text-3)" }}>ENC <strong style={{ color: g.encUtilPct > 0 ? "var(--success)" : "var(--text-2)" }}>{g.encUtilPct}%</strong></span>
|
||||||
|
)}
|
||||||
|
{g.memUsedMb != null && g.memMb && (
|
||||||
|
<span style={{ color: "var(--text-3)" }}>VRAM <strong style={{ color: "var(--text-2)" }}>{g.memUsedMb}/{g.memMb} MB</strong></span>
|
||||||
|
)}
|
||||||
|
{g.nvencSessions != null && (
|
||||||
|
<span style={{ color: "var(--text-3)" }}>NVENC <strong style={{ color: g.nvencSessions > 0 ? "var(--success)" : "var(--text-2)" }}>{g.nvencSessions}</strong></span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span style={{
|
<span style={{
|
||||||
fontSize: 10, fontWeight: 600, padding: "2px 6px", borderRadius: 3,
|
fontSize: 10, fontWeight: 600, padding: "2px 6px", borderRadius: 3,
|
||||||
|
|
@ -1379,6 +1743,9 @@ function Cluster() {
|
||||||
|
|
||||||
{/* ── Capture cards ── */}
|
{/* ── Capture cards ── */}
|
||||||
<BmdCardPanel sel={sel} portSignals={portSignals} />
|
<BmdCardPanel sel={sel} portSignals={portSignals} />
|
||||||
|
|
||||||
|
{/* ── Capture Drivers / SDKs ── */}
|
||||||
|
<DriverPanel sel={sel} />
|
||||||
<div style={{ display: "flex", gap: 6, marginTop: 6 }}>
|
<div style={{ display: "flex", gap: 6, marginTop: 6 }}>
|
||||||
<button className="btn ghost sm" onClick={() => nodeLogsHint(sel)}>Logs</button>
|
<button className="btn ghost sm" onClick={() => nodeLogsHint(sel)}>Logs</button>
|
||||||
<button className="btn ghost sm" onClick={() => drainNode(sel)}>Drain</button>
|
<button className="btn ghost sm" onClick={() => drainNode(sel)}>Drain</button>
|
||||||
|
|
@ -1420,23 +1787,15 @@ function Cluster() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddNodeModal — Approach A onboarding wizard. Collects a node name + role,
|
// AddNodeModal — Approach A onboarding wizard. Collects a node name, mints a
|
||||||
// mints a one-time auth token via /auth/tokens, and renders a ready-to-paste
|
// one-time auth token via /auth/tokens, and renders a ready-to-paste
|
||||||
// `curl … | bash` command that provisions the machine via deploy/onboard-node.sh.
|
// `curl … | bash` command that provisions the machine via deploy/onboard-node.sh.
|
||||||
//
|
//
|
||||||
// Role → compose PROFILES mapping (see docker-compose.worker.yml):
|
// No role picker: the new node self-detects its hardware (GPU / DeckLink /
|
||||||
// Worker → "worker"
|
// Deltacast) in onboard-node.sh and auto-enables the matching compose profiles
|
||||||
// Capture → "worker capture"
|
// (worker always; + gpu / + capture when present). Zero manual choice.
|
||||||
// GPU → "worker gpu" (worker-l4 service, profiles: [gpu])
|
|
||||||
const ADD_NODE_ROLES = [
|
|
||||||
{ id: 'worker', label: 'Worker', profiles: 'worker', desc: 'CPU transcode / general jobs' },
|
|
||||||
{ id: 'capture', label: 'Capture', profiles: 'worker capture', desc: 'SDI / DeckLink ingest' },
|
|
||||||
{ id: 'gpu', label: 'GPU', profiles: 'worker gpu', desc: 'NVENC-accelerated transcode' },
|
|
||||||
];
|
|
||||||
|
|
||||||
function AddNodeModal({ onClose }) {
|
function AddNodeModal({ onClose }) {
|
||||||
const [nodeName, setNodeName] = React.useState('');
|
const [nodeName, setNodeName] = React.useState('');
|
||||||
const [role, setRole] = React.useState('worker');
|
|
||||||
const [apiUrl, setApiUrl] = React.useState('');
|
const [apiUrl, setApiUrl] = React.useState('');
|
||||||
const [info, setInfo] = React.useState(null); // { scriptUrl, branch }
|
const [info, setInfo] = React.useState(null); // { scriptUrl, branch }
|
||||||
const [command, setCommand] = React.useState(null); // generated string
|
const [command, setCommand] = React.useState(null); // generated string
|
||||||
|
|
@ -1454,8 +1813,6 @@ function AddNodeModal({ onClose }) {
|
||||||
.catch(() => {}); // leave apiUrl empty → user must fill it before Generate
|
.catch(() => {}); // leave apiUrl empty → user must fill it before Generate
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const roleDef = ADD_NODE_ROLES.find(r => r.id === role) || ADD_NODE_ROLES[0];
|
|
||||||
|
|
||||||
const generate = async () => {
|
const generate = async () => {
|
||||||
setError(null);
|
setError(null);
|
||||||
if (!nodeName.trim()) { setError('Node name is required.'); return; }
|
if (!nodeName.trim()) { setError('Node name is required.'); return; }
|
||||||
|
|
@ -1477,8 +1834,7 @@ function AddNodeModal({ onClose }) {
|
||||||
const scriptUrl = (info && info.scriptUrl)
|
const scriptUrl = (info && info.scriptUrl)
|
||||||
|| 'https://forge.wilddragon.net/zgaetano/wild-dragon/raw/branch/main/deploy/onboard-node.sh';
|
|| 'https://forge.wilddragon.net/zgaetano/wild-dragon/raw/branch/main/deploy/onboard-node.sh';
|
||||||
const cmd =
|
const cmd =
|
||||||
`curl -sL ${scriptUrl} | NODE_TOKEN=${token} MAM_API_URL=${apiUrl.trim()} ` +
|
`curl -sL ${scriptUrl} | NODE_TOKEN=${token} MAM_API_URL=${apiUrl.trim()} bash`;
|
||||||
`NODE_ROLE=${role} PROFILES="${roleDef.profiles}" bash`;
|
|
||||||
setCommand(cmd);
|
setCommand(cmd);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(e.message || 'Network error');
|
setError(e.message || 'Network error');
|
||||||
|
|
@ -1511,21 +1867,6 @@ function AddNodeModal({ onClose }) {
|
||||||
value={nodeName} onChange={e => setNodeName(e.target.value)} />
|
value={nodeName} onChange={e => setNodeName(e.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ marginBottom: 14 }}>
|
|
||||||
<label style={{ display: 'block', fontSize: 11.5, color: 'var(--text-3)', marginBottom: 5 }}>Role</label>
|
|
||||||
<div style={{ display: 'flex', gap: 6 }}>
|
|
||||||
{ADD_NODE_ROLES.map(rd => (
|
|
||||||
<button key={rd.id}
|
|
||||||
className={'btn sm' + (role === rd.id ? ' primary' : ' ghost')}
|
|
||||||
style={{ flex: 1, flexDirection: 'column', alignItems: 'flex-start', gap: 2, padding: '8px 10px' }}
|
|
||||||
onClick={() => setRole(rd.id)}>
|
|
||||||
<span style={{ fontWeight: 600 }}>{rd.label}</span>
|
|
||||||
<span style={{ fontSize: 10, opacity: 0.8 }}>{rd.desc}</span>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ marginBottom: 4 }}>
|
<div style={{ marginBottom: 4 }}>
|
||||||
<label style={{ display: 'block', fontSize: 11.5, color: 'var(--text-3)', marginBottom: 5 }}>Primary API URL</label>
|
<label style={{ display: 'block', fontSize: 11.5, color: 'var(--text-3)', marginBottom: 5 }}>Primary API URL</label>
|
||||||
<input className="field-input mono" style={{ width: '100%', fontSize: 12 }}
|
<input className="field-input mono" style={{ width: '100%', fontSize: 12 }}
|
||||||
|
|
@ -1546,6 +1887,9 @@ function AddNodeModal({ onClose }) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<code className="mono" style={{ display: 'block', background: 'var(--bg-2)', padding: 12, borderRadius: 6, fontSize: 11.5, lineHeight: 1.5, wordBreak: 'break-all', whiteSpace: 'pre-wrap' }}>{command}</code>
|
<code className="mono" style={{ display: 'block', background: 'var(--bg-2)', padding: 12, borderRadius: 6, fontSize: 11.5, lineHeight: 1.5, wordBreak: 'break-all', whiteSpace: 'pre-wrap' }}>{command}</code>
|
||||||
|
<div style={{ fontSize: 11, color: 'var(--text-4)', marginTop: 8, lineHeight: 1.5 }}>
|
||||||
|
Profiles (worker / capture / GPU) are auto-selected from the new machine's detected hardware — no need to choose.
|
||||||
|
</div>
|
||||||
<ol style={{ margin: '12px 0 0', paddingLeft: 18, fontSize: 12, color: 'var(--text-2)', lineHeight: 1.6 }}>
|
<ol style={{ margin: '12px 0 0', paddingLeft: 18, fontSize: 12, color: 'var(--text-2)', lineHeight: 1.6 }}>
|
||||||
<li>SSH into the fresh Ubuntu machine.</li>
|
<li>SSH into the fresh Ubuntu machine.</li>
|
||||||
<li>Paste and run this command.</li>
|
<li>Paste and run this command.</li>
|
||||||
|
|
@ -2426,6 +2770,7 @@ function SdkVendorRow({ vendor, status, onDone }) {
|
||||||
const fileRef = React.useRef(null);
|
const fileRef = React.useRef(null);
|
||||||
const [uploading, setUploading] = React.useState(false);
|
const [uploading, setUploading] = React.useState(false);
|
||||||
const [progress, setProgress] = React.useState(0);
|
const [progress, setProgress] = React.useState(0);
|
||||||
|
const [confirm, confirmModal] = window.useConfirm();
|
||||||
|
|
||||||
const deployed = status && status.file_count > 0;
|
const deployed = status && status.file_count > 0;
|
||||||
const lastUpload = status?.uploaded_at
|
const lastUpload = status?.uploaded_at
|
||||||
|
|
@ -2467,8 +2812,8 @@ function SdkVendorRow({ vendor, status, onDone }) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const clear = () => {
|
const clear = async () => {
|
||||||
if (!confirm('Remove staged ' + vendor.name + ' SDK files?')) return;
|
if (!(await confirm({ title: 'Remove staged SDK files?', message: 'Remove staged ' + vendor.name + ' SDK files?', confirmLabel: 'Remove' }))) return;
|
||||||
window.ZAMPP_API.fetch('/sdk/' + vendor.id, { method: 'DELETE' })
|
window.ZAMPP_API.fetch('/sdk/' + vendor.id, { method: 'DELETE' })
|
||||||
.then(() => onDone(vendor.name + ': cleared.', true))
|
.then(() => onDone(vendor.name + ': cleared.', true))
|
||||||
.catch(e => onDone(vendor.name + ': ' + e.message, false));
|
.catch(e => onDone(vendor.name + ': ' + e.message, false));
|
||||||
|
|
@ -2476,6 +2821,7 @@ function SdkVendorRow({ vendor, status, onDone }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ border: '1px solid var(--border)', borderRadius: 6, padding: 12, background: 'var(--bg-2)' }}>
|
<div style={{ border: '1px solid var(--border)', borderRadius: 6, padding: 12, background: 'var(--bg-2)' }}>
|
||||||
|
{confirmModal}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
|
||||||
<strong style={{ fontSize: 13 }}>{vendor.name}</strong>
|
<strong style={{ fontSize: 13 }}>{vendor.name}</strong>
|
||||||
{deployed
|
{deployed
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ function AssetDetail({ asset, onClose }) {
|
||||||
const [comments, setComments] = React.useState([]);
|
const [comments, setComments] = React.useState([]);
|
||||||
const [newComment, setNewComment] = React.useState("");
|
const [newComment, setNewComment] = React.useState("");
|
||||||
const [commentsLoading, setCommentsLoading] = React.useState(false);
|
const [commentsLoading, setCommentsLoading] = React.useState(false);
|
||||||
|
const [confirm, confirmModal] = window.useConfirm();
|
||||||
|
|
||||||
// Stream / video state
|
// Stream / video state
|
||||||
const [streamUrl, setStreamUrl] = React.useState(null);
|
const [streamUrl, setStreamUrl] = React.useState(null);
|
||||||
|
|
@ -198,6 +199,45 @@ function AssetDetail({ asset, onClose }) {
|
||||||
// Pull a presigned hi-res URL and trigger a browser download with the
|
// Pull a presigned hi-res URL and trigger a browser download with the
|
||||||
// asset's display name as the filename. Falls back to opening in a new tab.
|
// asset's display name as the filename. Falls back to opening in a new tab.
|
||||||
const [downloading, setDownloading] = React.useState(false);
|
const [downloading, setDownloading] = React.useState(false);
|
||||||
|
|
||||||
|
// Gate the download behind a one-time "large file / connection speed"
|
||||||
|
// warning, shared with the library via the df.lib.download.warnDismissed
|
||||||
|
// localStorage flag. Once dismissed, downloads start without the prompt.
|
||||||
|
const dismissForeverRef = React.useRef(false);
|
||||||
|
const requestDownload = async function() {
|
||||||
|
if (downloading) return;
|
||||||
|
let dismissed = false;
|
||||||
|
try { dismissed = localStorage.getItem('df.lib.download.warnDismissed') === '1'; } catch (_) {}
|
||||||
|
if (!dismissed) {
|
||||||
|
dismissForeverRef.current = false;
|
||||||
|
const ok = await confirm({
|
||||||
|
title: 'Download original',
|
||||||
|
message: <div>
|
||||||
|
<div style={{ fontSize: 13, color: 'var(--text-2)', lineHeight: 1.5, marginBottom: 10 }}>
|
||||||
|
You're about to download the full-length original master for <b>{asset.name}</b>.
|
||||||
|
These files can be very large and download speed depends on your connection.
|
||||||
|
</div>
|
||||||
|
<label style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 12.5, color: 'var(--text-3)', cursor: 'pointer' }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
onChange={function(e) { dismissForeverRef.current = e.target.checked; }}
|
||||||
|
/>
|
||||||
|
Don't show this warning again
|
||||||
|
</label>
|
||||||
|
</div>,
|
||||||
|
confirmLabel: 'Download',
|
||||||
|
cancelLabel: 'Cancel',
|
||||||
|
danger: false,
|
||||||
|
});
|
||||||
|
if (!ok) return;
|
||||||
|
// Persist the dismissal only after the user confirms the download.
|
||||||
|
if (dismissForeverRef.current) {
|
||||||
|
try { localStorage.setItem('df.lib.download.warnDismissed', '1'); } catch (_) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
downloadHires();
|
||||||
|
};
|
||||||
|
|
||||||
const downloadHires = function() {
|
const downloadHires = function() {
|
||||||
if (downloading) return;
|
if (downloading) return;
|
||||||
setDownloading(true);
|
setDownloading(true);
|
||||||
|
|
@ -231,9 +271,12 @@ function AssetDetail({ asset, onClose }) {
|
||||||
if (navigator.clipboard) navigator.clipboard.writeText(assetId).catch(function() {});
|
if (navigator.clipboard) navigator.clipboard.writeText(assetId).catch(function() {});
|
||||||
setMenuOpen(false);
|
setMenuOpen(false);
|
||||||
};
|
};
|
||||||
const deleteAsset = function() {
|
const deleteAsset = async function() {
|
||||||
setMenuOpen(false);
|
setMenuOpen(false);
|
||||||
if (!window.confirm('Delete "' + (asset.name || assetId) + '" permanently?\nRemoves DB row + S3 objects. Cannot be undone.')) return;
|
if (!(await confirm({
|
||||||
|
title: 'Delete asset?',
|
||||||
|
message: 'Delete "' + (asset.name || assetId) + '" permanently?\nRemoves DB row + S3 objects. Cannot be undone.',
|
||||||
|
}))) return;
|
||||||
window.ZAMPP_API.fetch('/assets/' + assetId + '?hard=true', { method: 'DELETE' })
|
window.ZAMPP_API.fetch('/assets/' + assetId + '?hard=true', { method: 'DELETE' })
|
||||||
.then(function() { onClose && onClose(); })
|
.then(function() { onClose && onClose(); })
|
||||||
.catch(function(e) { window.alert('Delete failed: ' + e.message); });
|
.catch(function(e) { window.alert('Delete failed: ' + e.message); });
|
||||||
|
|
@ -325,8 +368,8 @@ function AssetDetail({ asset, onClose }) {
|
||||||
.catch(function() {});
|
.catch(function() {});
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteComment = function(c) {
|
const deleteComment = async function(c) {
|
||||||
if (!confirm('Delete this comment?')) return;
|
if (!(await confirm({ title: 'Delete comment?', message: 'Delete this comment?' }))) return;
|
||||||
window.ZAMPP_API.fetch('/assets/' + assetId + '/comments/' + c.id, { method: 'DELETE' })
|
window.ZAMPP_API.fetch('/assets/' + assetId + '/comments/' + c.id, { method: 'DELETE' })
|
||||||
.then(function() {
|
.then(function() {
|
||||||
setComments(function(prev) { return prev.filter(x => x.id !== c.id); });
|
setComments(function(prev) { return prev.filter(x => x.id !== c.id); });
|
||||||
|
|
@ -355,6 +398,7 @@ function AssetDetail({ asset, onClose }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="asset-detail fade-in">
|
<div className="asset-detail fade-in">
|
||||||
|
{confirmModal}
|
||||||
<div className="asset-detail-header">
|
<div className="asset-detail-header">
|
||||||
<button className="icon-btn" aria-label="Back" onClick={onClose}><Icon name="arrowLeft" /></button>
|
<button className="icon-btn" aria-label="Back" onClick={onClose}><Icon name="arrowLeft" /></button>
|
||||||
<div style={{ display: "flex", flexDirection: "column", minWidth: 0 }}>
|
<div style={{ display: "flex", flexDirection: "column", minWidth: 0 }}>
|
||||||
|
|
@ -367,9 +411,11 @@ function AssetDetail({ asset, onClose }) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1 }} />
|
<div style={{ flex: 1 }} />
|
||||||
<button className="btn ghost sm" onClick={downloadHires} disabled={downloading} title="Download the hi-res master file">
|
{asset.original_s3_key && (
|
||||||
<Icon name="download" />{downloading ? 'Preparing…' : 'Download'}
|
<button className="btn ghost sm" onClick={requestDownload} disabled={downloading} title="Download the hi-res master file">
|
||||||
</button>
|
<Icon name="download" />{downloading ? 'Preparing…' : 'Download'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<div style={{ position: 'relative' }}>
|
<div style={{ position: 'relative' }}>
|
||||||
<button ref={moreBtnRef} className="icon-btn" aria-label="More actions" onClick={function(e) { e.stopPropagation(); setMenuOpen(function(v) { return !v; }); }}>
|
<button ref={moreBtnRef} className="icon-btn" aria-label="More actions" onClick={function(e) { e.stopPropagation(); setMenuOpen(function(v) { return !v; }); }}>
|
||||||
<Icon name="more" />
|
<Icon name="more" />
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ function Editor() {
|
||||||
const [projectId, setProjectId] = React.useState(null);
|
const [projectId, setProjectId] = React.useState(null);
|
||||||
const [sequences, setSequences] = React.useState([]);
|
const [sequences, setSequences] = React.useState([]);
|
||||||
const [currentSeq, setCurrentSeq] = React.useState(null);
|
const [currentSeq, setCurrentSeq] = React.useState(null);
|
||||||
|
const [confirm, confirmModal] = window.useConfirm();
|
||||||
const [assets, setAssets] = React.useState([]);
|
const [assets, setAssets] = React.useState([]);
|
||||||
const [bins, setBins] = React.useState([]);
|
const [bins, setBins] = React.useState([]);
|
||||||
const [sourceAsset, setSourceAsset] = React.useState(null);
|
const [sourceAsset, setSourceAsset] = React.useState(null);
|
||||||
|
|
@ -159,7 +160,7 @@ function Editor() {
|
||||||
|
|
||||||
async function deleteSequence() {
|
async function deleteSequence() {
|
||||||
if (!currentSeq) return;
|
if (!currentSeq) return;
|
||||||
if (!window.confirm('Delete sequence "' + currentSeq.name + '"? This cannot be undone.')) return;
|
if (!(await confirm({ title: 'Delete sequence?', message: 'Delete sequence "' + currentSeq.name + '"? This cannot be undone.' }))) return;
|
||||||
try {
|
try {
|
||||||
await window.ZAMPP_API.deleteSequence(currentSeq.id);
|
await window.ZAMPP_API.deleteSequence(currentSeq.id);
|
||||||
const remaining = sequences.filter(s => s.id !== currentSeq.id);
|
const remaining = sequences.filter(s => s.id !== currentSeq.id);
|
||||||
|
|
@ -377,6 +378,7 @@ function Editor() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="editor-shell" style={{ position: 'relative', display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
|
<div className="editor-shell" style={{ position: 'relative', display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
|
||||||
|
{confirmModal}
|
||||||
|
|
||||||
{/* Beta banner: flat strip across top, no glassmorphism or gradient. */}
|
{/* Beta banner: flat strip across top, no glassmorphism or gradient. */}
|
||||||
<div className="editor-beta-banner">
|
<div className="editor-beta-banner">
|
||||||
|
|
|
||||||
|
|
@ -23,12 +23,19 @@ function Home({ navigate }) {
|
||||||
// Pull live counts so the tile subtitles ("34 assets", "0 live", "3 running")
|
// Pull live counts so the tile subtitles ("34 assets", "0 live", "3 running")
|
||||||
// reflect what's actually in the DB right now, not a stale boot-time cache.
|
// reflect what's actually in the DB right now, not a stale boot-time cache.
|
||||||
const [cards, setCards] = React.useState({});
|
const [cards, setCards] = React.useState({});
|
||||||
|
// Playout has no /metrics/home card yet (and the playout schema may not be
|
||||||
|
// migrated on every install); fetch /playout/channels separately and degrade
|
||||||
|
// silently — the tile just shows "No channels" if the endpoint isn't there.
|
||||||
|
const [playoutChannels, setPlayoutChannels] = React.useState(null);
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
const load = () => {
|
const load = () => {
|
||||||
window.ZAMPP_API.fetch('/metrics/home?hours=1')
|
window.ZAMPP_API.fetch('/metrics/home?hours=1')
|
||||||
.then(d => { if (!cancelled) setCards(d?.cards || {}); })
|
.then(d => { if (!cancelled) setCards(d?.cards || {}); })
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
|
window.ZAMPP_API.fetch('/playout/channels')
|
||||||
|
.then(d => { if (!cancelled) setPlayoutChannels(Array.isArray(d) ? d : []); })
|
||||||
|
.catch(() => { if (!cancelled) setPlayoutChannels([]); });
|
||||||
};
|
};
|
||||||
load();
|
load();
|
||||||
const t = setInterval(load, 30_000);
|
const t = setInterval(load, 30_000);
|
||||||
|
|
@ -67,17 +74,24 @@ function Home({ navigate }) {
|
||||||
id: 'playout',
|
id: 'playout',
|
||||||
label: 'Playout',
|
label: 'Playout',
|
||||||
icon: 'signal',
|
icon: 'signal',
|
||||||
tone: 'live',
|
tone: 'accent',
|
||||||
sub: 'Master Control',
|
sub: (() => {
|
||||||
desc: 'Play assets to SDI, NDI, SRT or RTMP via CasparCG.',
|
if (playoutChannels === null) return '·';
|
||||||
|
const total = playoutChannels.length;
|
||||||
|
const onAir = playoutChannels.filter(c => c.status === 'running').length;
|
||||||
|
if (total === 0) return 'No channels';
|
||||||
|
if (onAir > 0) return onAir + ' on air · ' + total + ' channel' + (total === 1 ? '' : 's');
|
||||||
|
return total + ' channel' + (total === 1 ? '' : 's');
|
||||||
|
})(),
|
||||||
|
desc: 'Master Control. SDI · NDI · SRT · RTMP playout, playlists, as-run.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '__downloads',
|
id: '__downloads',
|
||||||
label: 'Downloads',
|
label: 'Downloads',
|
||||||
icon: 'download',
|
icon: 'download',
|
||||||
tone: 'purple',
|
tone: 'purple',
|
||||||
sub: 'Premiere panel · Dragon-ISO',
|
sub: 'Plugin · Teams ISO',
|
||||||
desc: 'Download the Premiere Pro panel and Dragon-ISO NDI tools.',
|
desc: 'Download the Premiere Pro UXP plugin and the Teams ISO installer.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'jobs',
|
id: 'jobs',
|
||||||
|
|
@ -270,14 +284,20 @@ function Home({ navigate }) {
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="launcher-footer">Created by Wild Dragon LLC</div>
|
||||||
</div>
|
</div>
|
||||||
{showDownloads && <DownloadsModal onClose={() => setShowDownloads(false)} />}
|
{showDownloads && <DownloadsModal onClose={() => setShowDownloads(false)} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Combined downloads modal: Premiere Pro panel + Dragon-ISO.
|
// Modal listing all downloads: the Premiere Pro UXP plugin (.ccx, one per
|
||||||
|
// released version, sourced from window.PREMIERE_RELEASES written by the
|
||||||
|
// Settings → SDKs section in screens-admin.jsx) plus the Teams ISO installer
|
||||||
|
// (window.TEAMS_ISO; the .exe slot is wired but the file may still be pending).
|
||||||
function DownloadsModal({ onClose }) {
|
function DownloadsModal({ onClose }) {
|
||||||
|
const teamsIso = window.TEAMS_ISO || {};
|
||||||
const releases = (window.PREMIERE_RELEASES || []).slice().sort((a, b) => {
|
const releases = (window.PREMIERE_RELEASES || []).slice().sort((a, b) => {
|
||||||
const av = String(a.version || ''), bv = String(b.version || '');
|
const av = String(a.version || ''), bv = String(b.version || '');
|
||||||
return bv.localeCompare(av, undefined, { numeric: true });
|
return bv.localeCompare(av, undefined, { numeric: true });
|
||||||
|
|
@ -293,20 +313,37 @@ function DownloadsModal({ onClose }) {
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontSize: 15, fontWeight: 600 }}>Downloads</div>
|
<div style={{ fontSize: 15, fontWeight: 600 }}>Downloads</div>
|
||||||
<div style={{ fontSize: 12, color: 'var(--text-3)', marginTop: 2 }}>
|
<div style={{ fontSize: 12, color: 'var(--text-3)', marginTop: 2 }}>
|
||||||
Premiere Pro panel and Dragon-ISO NDI tools.
|
The Premiere Pro (UXP) plugin and the Teams ISO installer. Install the .ccx per workstation via the Adobe UXP Developer Tool, or double-click it with Creative Cloud installed.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button className="icon-btn" aria-label="Close" onClick={onClose}><Icon name="x" /></button>
|
<button className="icon-btn" aria-label="Close" onClick={onClose}><Icon name="x" /></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="modal-body">
|
<div className="modal-body">
|
||||||
{/* ── Premiere panel ── */}
|
<div className="premiere-release">
|
||||||
<div className="downloads-section-head">
|
<div className="premiere-release-head">
|
||||||
<Icon name="editor" size={13} />
|
<span className="premiere-release-version mono">Teams ISO</span>
|
||||||
<span>Premiere Pro panel (UXP)</span>
|
{teamsIso.version && (
|
||||||
</div>
|
<span className="premiere-release-date mono">v{teamsIso.version}</span>
|
||||||
<div style={{ fontSize: 12, color: 'var(--text-3)', marginBottom: 10 }}>
|
)}
|
||||||
Install the .ccx per workstation via the Adobe UXP Developer Tool, or double-click it with Creative Cloud installed.
|
</div>
|
||||||
|
<div className="premiere-release-notes">
|
||||||
|
Windows installer for the Teams ISO workstation build.
|
||||||
|
</div>
|
||||||
|
<div className="premiere-release-actions">
|
||||||
|
{teamsIso.available && teamsIso.url ? (
|
||||||
|
<a href={teamsIso.url} download className="btn primary sm">
|
||||||
|
<Icon name="download" />Teams ISO (.exe)
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span className="btn primary sm" aria-disabled="true" style={{ opacity: 0.5, pointerEvents: 'none' }}>
|
||||||
|
<Icon name="download" />Teams ISO (.exe)
|
||||||
|
</span>
|
||||||
|
<span style={{ fontSize: 11.5, color: 'var(--text-3)' }}>coming soon — file pending</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{releases.length === 0 && (
|
{releases.length === 0 && (
|
||||||
<div style={{ padding: '12px 0', color: 'var(--text-3)', fontSize: 12 }}>
|
<div style={{ padding: '12px 0', color: 'var(--text-3)', fontSize: 12 }}>
|
||||||
|
|
|
||||||
|
|
@ -491,22 +491,29 @@ function HlsPreview({ assetId, recorderId, muted = true, controls = false, class
|
||||||
|
|
||||||
/* ===== Recorders ===== */
|
/* ===== Recorders ===== */
|
||||||
function _normRecorder(r) {
|
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 || {};
|
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 {
|
return {
|
||||||
...r,
|
...r,
|
||||||
source: r.source_type || '·',
|
source: r.source_type || '·',
|
||||||
url: cfg.url || cfg.address || cfg.srt_url || cfg.rtmp_url || r.source_type || '·',
|
url: cfg.url || cfg.address || cfg.srt_url || cfg.rtmp_url || r.source_type || '·',
|
||||||
codec: r.recording_codec || '·',
|
codec: r.recording_codec || '·',
|
||||||
res: r.recording_resolution || '·',
|
res: r.recording_resolution || '·',
|
||||||
|
framerate: r.recording_framerate || 'native',
|
||||||
node: r.node_id ? r.node_id.slice(0, 8) : 'primary',
|
node: r.node_id ? r.node_id.slice(0, 8) : 'primary',
|
||||||
elapsed,
|
capturePort,
|
||||||
|
elapsed: '·',
|
||||||
bitrate: '·',
|
bitrate: '·',
|
||||||
health: 100,
|
health: 100,
|
||||||
audio: false,
|
audio: false,
|
||||||
|
|
@ -587,6 +594,7 @@ function RecorderRow({ recorder: initialRecorder, onRefresh }) {
|
||||||
const [clipName, setClipName] = React.useState('');
|
const [clipName, setClipName] = React.useState('');
|
||||||
// Project override for this take. Defaults to the recorder's configured project.
|
// Project override for this take. Defaults to the recorder's configured project.
|
||||||
const [takeProjectId, setTakeProjectId] = React.useState(initialRecorder.project_id || PROJECTS[0]?.id || '');
|
const [takeProjectId, setTakeProjectId] = React.useState(initialRecorder.project_id || PROJECTS[0]?.id || '');
|
||||||
|
const [confirm, confirmModal] = window.useConfirm();
|
||||||
const isRec = recorder.status === 'recording';
|
const isRec = recorder.status === 'recording';
|
||||||
|
|
||||||
// Keep takeProjectId in sync if the recorder row changes (e.g. after a refresh).
|
// Keep takeProjectId in sync if the recorder row changes (e.g. after a refresh).
|
||||||
|
|
@ -609,15 +617,43 @@ function RecorderRow({ recorder: initialRecorder, onRefresh }) {
|
||||||
return () => clearInterval(id);
|
return () => clearInterval(id);
|
||||||
}, [isRec, recorder.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(() => {
|
const displayElapsed = React.useMemo(() => {
|
||||||
if (liveStatus && liveStatus.duration != null) {
|
if (!isRec) return '·';
|
||||||
const d = Math.max(0, liveStatus.duration);
|
const d = Math.max(0, elapsedSecs);
|
||||||
return String(Math.floor(d / 3600)).padStart(2, '0') + ':' +
|
return String(Math.floor(d / 3600)).padStart(2, '0') + ':' +
|
||||||
String(Math.floor((d % 3600) / 60)).padStart(2, '0') + ':' +
|
String(Math.floor((d % 3600) / 60)).padStart(2, '0') + ':' +
|
||||||
String(d % 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;
|
return recorder.framerate || 'native';
|
||||||
}, [liveStatus, recorder.elapsed]);
|
}, [isRec, liveStatus, recorder.framerate]);
|
||||||
|
|
||||||
const displaySignal = liveStatus
|
const displaySignal = liveStatus
|
||||||
? (liveStatus.signal || '·')
|
? (liveStatus.signal || '·')
|
||||||
|
|
@ -657,8 +693,8 @@ function RecorderRow({ recorder: initialRecorder, onRefresh }) {
|
||||||
.catch(e => { setPending(false); setErr(e.message || 'Failed'); setRecorder(initialRecorder); });
|
.catch(e => { setPending(false); setErr(e.message || 'Failed'); setRecorder(initialRecorder); });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = async () => {
|
||||||
if (!window.confirm('Delete recorder "' + recorder.name + '"?\nThis will stop any active recording and cannot be undone.')) return;
|
if (!(await confirm({ title: 'Delete recorder?', message: 'Delete recorder "' + recorder.name + '"?\nThis will stop any active recording and cannot be undone.' }))) return;
|
||||||
window.ZAMPP_API.fetch('/recorders/' + recorder.id, { method: 'DELETE' })
|
window.ZAMPP_API.fetch('/recorders/' + recorder.id, { method: 'DELETE' })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
onRefresh();
|
onRefresh();
|
||||||
|
|
@ -670,6 +706,7 @@ function RecorderRow({ recorder: initialRecorder, onRefresh }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'recorder-row ' + recorder.status}>
|
<div className={'recorder-row ' + recorder.status}>
|
||||||
|
{confirmModal}
|
||||||
<div className="recorder-preview">
|
<div className="recorder-preview">
|
||||||
{isRec && recorder.live_asset_id
|
{isRec && recorder.live_asset_id
|
||||||
? <HlsPreview assetId={recorder.live_asset_id} recorderId={recorder.id} />
|
? <HlsPreview assetId={recorder.live_asset_id} recorderId={recorder.id} />
|
||||||
|
|
@ -684,6 +721,11 @@ function RecorderRow({ recorder: initialRecorder, onRefresh }) {
|
||||||
<StatusDot status={recorder.status} /> {recorder.status.toUpperCase()}
|
<StatusDot status={recorder.status} /> {recorder.status.toUpperCase()}
|
||||||
</span>
|
</span>
|
||||||
<span className="badge outline">{recorder.source}</span>
|
<span className="badge outline">{recorder.source}</span>
|
||||||
|
{recorder.capturePort && (
|
||||||
|
<span className="badge outline" title="Capture port" style={{ background: 'rgba(74,158,255,0.12)', borderColor: 'rgba(74,158,255,0.4)', color: 'var(--accent)' }}>
|
||||||
|
<Icon name="signal" size={10} style={{ marginRight: 4, verticalAlign: -1 }} />{recorder.capturePort}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="recorder-sub mono">{recorder.url}</div>
|
<div className="recorder-sub mono">{recorder.url}</div>
|
||||||
<div className="recorder-sub">
|
<div className="recorder-sub">
|
||||||
|
|
@ -707,12 +749,10 @@ function RecorderRow({ recorder: initialRecorder, onRefresh }) {
|
||||||
{displaySignal}
|
{displaySignal}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{liveStatus?.currentFps != null && (
|
<div className="recorder-stat">
|
||||||
<div className="recorder-stat">
|
<div className="stat-label">Framerate</div>
|
||||||
<div className="stat-label">FPS</div>
|
<div className="stat-val mono">{displayFramerate}</div>
|
||||||
<div className="stat-val mono">{Number(liveStatus.currentFps).toFixed(1)}</div>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="recorder-actions">
|
<div className="recorder-actions">
|
||||||
{!isRec && (
|
{!isRec && (
|
||||||
|
|
@ -997,6 +1037,7 @@ function Capture({ navigate }) {
|
||||||
/* ===== Monitors ===== */
|
/* ===== Monitors ===== */
|
||||||
function Monitors({ navigate }) {
|
function Monitors({ navigate }) {
|
||||||
const [recorders, setRecorders] = React.useState(window.ZAMPP_DATA?.RECORDERS || []);
|
const [recorders, setRecorders] = React.useState(window.ZAMPP_DATA?.RECORDERS || []);
|
||||||
|
const [channels, setChannels] = React.useState([]);
|
||||||
const [grid, setGrid] = React.useState(4);
|
const [grid, setGrid] = React.useState(4);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
|
@ -1008,6 +1049,11 @@ function Monitors({ navigate }) {
|
||||||
setRecorders(norm);
|
setRecorders(norm);
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
|
// Playout channels surface here too so an operator can watch on-air
|
||||||
|
// output alongside ingest. Degrade silently if the endpoint is absent.
|
||||||
|
window.ZAMPP_API.fetch('/playout/channels')
|
||||||
|
.then(raw => setChannels(Array.isArray(raw) ? raw : []))
|
||||||
|
.catch(() => setChannels([]));
|
||||||
};
|
};
|
||||||
refresh();
|
refresh();
|
||||||
const id = setInterval(refresh, 5000);
|
const id = setInterval(refresh, 5000);
|
||||||
|
|
@ -1032,18 +1078,87 @@ function Monitors({ navigate }) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="page-body">
|
<div className="page-body">
|
||||||
{feeds.length === 0 ? (
|
{feeds.length === 0 && channels.length === 0 ? (
|
||||||
<div style={{ padding: 60, textAlign: 'center', color: 'var(--text-3)' }}>No active feeds. Start a recorder to see live video here.</div>
|
<div style={{ padding: 60, textAlign: 'center', color: 'var(--text-3)' }}>No active feeds. Start a recorder or playout channel to see live video here.</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="monitors-grid" style={{ display: 'grid', gridTemplateColumns: 'repeat(' + grid + ', 1fr)', gap: 8 }}>
|
<React.Fragment>
|
||||||
{feeds.map((f, i) => <MonitorTile key={f.id} feed={f} seed={i + 1} />)}
|
{feeds.length > 0 && (
|
||||||
</div>
|
<React.Fragment>
|
||||||
|
<div className="monitor-section-head">Ingest</div>
|
||||||
|
<div className="monitors-grid" style={{ display: 'grid', gridTemplateColumns: 'repeat(' + grid + ', 1fr)', gap: 8 }}>
|
||||||
|
{feeds.map((f, i) => <MonitorTile key={f.id} feed={f} seed={i + 1} />)}
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
{channels.length > 0 && (
|
||||||
|
<React.Fragment>
|
||||||
|
<div className="monitor-section-head">Playout</div>
|
||||||
|
<div className="monitors-grid" style={{ display: 'grid', gridTemplateColumns: 'repeat(' + grid + ', 1fr)', gap: 8 }}>
|
||||||
|
{channels.slice(0, grid * grid).map(c => <PlayoutMonitorTile key={c.id} channel={c} />)}
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function PlayoutMonitorTile({ channel }) {
|
||||||
|
const videoRef = React.useRef(null);
|
||||||
|
const hlsRef = React.useRef(null);
|
||||||
|
const onAir = channel.status === 'running';
|
||||||
|
const previewUrl = '/api/v1/playout/channels/' + channel.id + '/hls/index.m3u8';
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const vid = videoRef.current;
|
||||||
|
if (!vid) return;
|
||||||
|
if (hlsRef.current) { try { hlsRef.current.destroy(); } catch (_) {} hlsRef.current = null; }
|
||||||
|
if (!onAir) { vid.src = ''; return; }
|
||||||
|
|
||||||
|
if (window.Hls && window.Hls.isSupported()) {
|
||||||
|
const hls = new window.Hls({
|
||||||
|
liveSyncDurationCount: 3,
|
||||||
|
liveMaxLatencyDurationCount: 6,
|
||||||
|
xhrSetup: (xhr) => { xhr.withCredentials = true; },
|
||||||
|
});
|
||||||
|
hlsRef.current = hls;
|
||||||
|
hls.loadSource(previewUrl);
|
||||||
|
hls.attachMedia(vid);
|
||||||
|
hls.on(window.Hls.Events.MANIFEST_PARSED, () => vid.play().catch(() => {}));
|
||||||
|
} else if (vid.canPlayType('application/vnd.apple.mpegurl')) {
|
||||||
|
vid.src = previewUrl;
|
||||||
|
vid.play().catch(() => {});
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
if (hlsRef.current) { try { hlsRef.current.destroy(); } catch (_) {} hlsRef.current = null; }
|
||||||
|
};
|
||||||
|
}, [onAir, channel.id]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="monitor-tile">
|
||||||
|
{onAir ? (
|
||||||
|
<video ref={videoRef} muted playsInline autoPlay
|
||||||
|
style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block', background: '#000' }} />
|
||||||
|
) : (
|
||||||
|
<FauxFrame />
|
||||||
|
)}
|
||||||
|
{onAir && <div style={{ position: 'absolute', inset: 0, border: '2px solid var(--live)', pointerEvents: 'none', borderRadius: 'inherit' }} />}
|
||||||
|
{!onAir && (
|
||||||
|
<div style={{ position: 'absolute', inset: 0, display: 'grid', placeItems: 'center', color: 'var(--text-3)', fontSize: 11 }}>channel idle</div>
|
||||||
|
)}
|
||||||
|
<div style={{ position: 'absolute', top: 8, left: 8, display: 'flex', gap: 6 }}>
|
||||||
|
{onAir ? <span className="badge live">ON AIR</span> : <span className="badge neutral">IDLE</span>}
|
||||||
|
</div>
|
||||||
|
<div className="monitor-tile-label">
|
||||||
|
<span className="name">{channel.name}</span>
|
||||||
|
{channel.output_type && <span className="time mono">{String(channel.output_type).toUpperCase()}</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function MonitorTile({ feed, seed }) {
|
function MonitorTile({ feed, seed }) {
|
||||||
const [levels, setLevels] = React.useState([0.65, 0.78]);
|
const [levels, setLevels] = React.useState([0.65, 0.78]);
|
||||||
const isLive = feed.status === 'recording';
|
const isLive = feed.status === 'recording';
|
||||||
|
|
@ -1374,29 +1489,38 @@ function _EventBlock({ event, recorder, dayStart, dayEnd, pph, now, projects, on
|
||||||
const left = (startMin / 60) * pph;
|
const left = (startMin / 60) * pph;
|
||||||
const width = Math.max(40, ((endMin - startMin) / 60) * pph);
|
const width = Math.max(40, ((endMin - startMin) / 60) * pph);
|
||||||
|
|
||||||
|
// Schedules backed by a recorder configured to write a growing file get a
|
||||||
|
// green accent so operators can tell at a glance which slots will produce
|
||||||
|
// an edit-while-recording deliverable (vs. close-then-publish).
|
||||||
|
const isGrowing = !!(recorder && recorder.growing_enabled);
|
||||||
|
|
||||||
const classes = ['epg-block'];
|
const classes = ['epg-block'];
|
||||||
if (isLive) classes.push('live');
|
if (isLive) classes.push('live');
|
||||||
if (isFailed) classes.push('failed');
|
if (isFailed) classes.push('failed');
|
||||||
else if (isPast) classes.push('past');
|
else if (isPast) classes.push('past');
|
||||||
if (drag && drag.moved) classes.push('dragging');
|
if (drag && drag.moved) classes.push('dragging');
|
||||||
if (canDrag) classes.push('resizable');
|
if (canDrag) classes.push('resizable');
|
||||||
|
if (isGrowing) classes.push('growing');
|
||||||
|
|
||||||
|
const blockColor = isGrowing ? '#2ecc71' : (color || 'var(--text-3)');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={blockRef}
|
ref={blockRef}
|
||||||
className={classes.join(' ')}
|
className={classes.join(' ')}
|
||||||
style={{ left, width, '--epg-block-color': color || 'var(--text-3)' }}
|
style={{ left, width, '--epg-block-color': blockColor }}
|
||||||
onContextMenu={(ev) => { ev.preventDefault(); ev.stopPropagation(); onContextMenu(event, ev); }}
|
onContextMenu={(ev) => { ev.preventDefault(); ev.stopPropagation(); onContextMenu(event, ev); }}
|
||||||
onPointerMove={onPointerMove}
|
onPointerMove={onPointerMove}
|
||||||
onPointerUp={endDrag}
|
onPointerUp={endDrag}
|
||||||
onPointerCancel={endDrag}
|
onPointerCancel={endDrag}
|
||||||
title={event.name + ' · ' + _fmtTime(dispStart) + ' → ' + _fmtTime(dispEnd) + (event.error_message ? ' · ' + event.error_message : '')}>
|
title={event.name + (isGrowing ? ' · GROWING' : '') + ' · ' + _fmtTime(dispStart) + ' → ' + _fmtTime(dispEnd) + (event.error_message ? ' · ' + event.error_message : '')}>
|
||||||
<span className="epg-block-bar" />
|
<span className="epg-block-bar" />
|
||||||
{/* Body click → edit, body drag → move. We hang the click on pointerup
|
{/* Body click → edit, body drag → move. We hang the click on pointerup
|
||||||
so the threshold check above can demote a drag back to a click. */}
|
so the threshold check above can demote a drag back to a click. */}
|
||||||
<div className="epg-block-body" onPointerDown={(ev) => startDrag(ev, 'body')}>
|
<div className="epg-block-body" onPointerDown={(ev) => startDrag(ev, 'body')}>
|
||||||
<span className="epg-block-name">{event.name}</span>
|
<span className="epg-block-name">{event.name}</span>
|
||||||
<span className="epg-block-time mono">{_fmtTime(dispStart)}{drag && drag.moved ? ' → ' + _fmtTime(dispEnd) : ''}</span>
|
<span className="epg-block-time mono">{_fmtTime(dispStart)}{drag && drag.moved ? ' → ' + _fmtTime(dispEnd) : ''}</span>
|
||||||
|
{isGrowing && <span className="epg-block-glyph growing" title="growing file (edit-while-record)" style={{ color: '#2ecc71' }}>▶</span>}
|
||||||
{isLive && <span className="epg-block-glyph live" title="on air">●</span>}
|
{isLive && <span className="epg-block-glyph live" title="on air">●</span>}
|
||||||
{isFailed && <span className="epg-block-glyph failed" title={event.error_message || 'failed'}>!</span>}
|
{isFailed && <span className="epg-block-glyph failed" title={event.error_message || 'failed'}>!</span>}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1525,6 +1649,7 @@ function Schedule({ navigate }) {
|
||||||
const [newDefaults, setNewDefaults] = React.useState(null);
|
const [newDefaults, setNewDefaults] = React.useState(null);
|
||||||
const [editing, setEditing] = React.useState(null);
|
const [editing, setEditing] = React.useState(null);
|
||||||
const [ctxMenu, setCtxMenu] = React.useState(null); // { schedule, x, y }
|
const [ctxMenu, setCtxMenu] = React.useState(null); // { schedule, x, y }
|
||||||
|
const [confirm, confirmModal] = window.useConfirm();
|
||||||
const [view, setView] = React.useState('today'); // 'today' | 'week' | 'list'
|
const [view, setView] = React.useState('today'); // 'today' | 'week' | 'list'
|
||||||
const [day, setDay] = React.useState(() => _dayStart(new Date()));
|
const [day, setDay] = React.useState(() => _dayStart(new Date()));
|
||||||
const [listFilter, setListFilter] = React.useState('upcoming');
|
const [listFilter, setListFilter] = React.useState('upcoming');
|
||||||
|
|
@ -1601,12 +1726,12 @@ function Schedule({ navigate }) {
|
||||||
};
|
};
|
||||||
const openNewBlank = () => { setNewDefaults(null); setShowNew(true); };
|
const openNewBlank = () => { setNewDefaults(null); setShowNew(true); };
|
||||||
|
|
||||||
const cancel = (s) => {
|
const cancel = async (s) => {
|
||||||
if (!confirm('Cancel scheduled recording "' + s.name + '"?')) return;
|
if (!(await confirm({ title: 'Cancel scheduled recording?', message: 'Cancel scheduled recording "' + s.name + '"?', confirmLabel: 'Cancel recording' }))) return;
|
||||||
window.ZAMPP_API.fetch('/schedules/' + s.id + '/cancel', { method: 'POST' }).then(load).catch(e => alert('Cancel failed: ' + e.message));
|
window.ZAMPP_API.fetch('/schedules/' + s.id + '/cancel', { method: 'POST' }).then(load).catch(e => alert('Cancel failed: ' + e.message));
|
||||||
};
|
};
|
||||||
const remove = (s) => {
|
const remove = async (s) => {
|
||||||
if (!confirm('Delete schedule "' + s.name + '"?')) return;
|
if (!(await confirm({ title: 'Delete schedule?', message: 'Delete schedule "' + s.name + '"?' }))) return;
|
||||||
window.ZAMPP_API.fetch('/schedules/' + s.id, { method: 'DELETE' }).then(load).catch(e => alert('Delete failed: ' + e.message));
|
window.ZAMPP_API.fetch('/schedules/' + s.id, { method: 'DELETE' }).then(load).catch(e => alert('Delete failed: ' + e.message));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1657,6 +1782,7 @@ function Schedule({ navigate }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="epg-page" style={{ '--epg-pph': pph + 'px' }}>
|
<div className="epg-page" style={{ '--epg-pph': pph + 'px' }}>
|
||||||
|
{confirmModal}
|
||||||
<_StatusStrip schedules={schedules || []} recorders={recorders} now={now} projects={projects} />
|
<_StatusStrip schedules={schedules || []} recorders={recorders} now={now} projects={projects} />
|
||||||
|
|
||||||
<div className="epg-toolbar">
|
<div className="epg-toolbar">
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ function Jobs({ navigate }) {
|
||||||
const [tab, setTab] = React.useState('all');
|
const [tab, setTab] = React.useState('all');
|
||||||
const [jobs, setJobs] = React.useState(window.ZAMPP_DATA.JOBS);
|
const [jobs, setJobs] = React.useState(window.ZAMPP_DATA.JOBS);
|
||||||
const [lastFetch, setLastFetch] = React.useState(Date.now());
|
const [lastFetch, setLastFetch] = React.useState(Date.now());
|
||||||
|
const [confirm, confirmModal] = window.useConfirm();
|
||||||
|
|
||||||
const normalizeJob = (j) => {
|
const normalizeJob = (j) => {
|
||||||
const statusMap = { waiting: 'queued', active: 'running', completed: 'done', failed: 'failed' };
|
const statusMap = { waiting: 'queued', active: 'running', completed: 'done', failed: 'failed' };
|
||||||
|
|
@ -87,34 +88,47 @@ function Jobs({ navigate }) {
|
||||||
// stalled-active job (worker died mid-process, holding a concurrency slot)
|
// stalled-active job (worker died mid-process, holding a concurrency slot)
|
||||||
// gets yanked and the next queued job runs. mode just changes the prompt
|
// gets yanked and the next queued job runs. mode just changes the prompt
|
||||||
// copy so the operator knows what they're doing.
|
// copy so the operator knows what they're doing.
|
||||||
const handleDelete = React.useCallback((job, mode) => {
|
const handleDelete = React.useCallback(async (job, mode) => {
|
||||||
const msg = mode === 'cancel'
|
const msg = mode === 'cancel'
|
||||||
? 'Cancel this running ' + job.kind + ' job?\n\nThe worker may run a few seconds longer in the background, but its result will be discarded and the queue slot frees up immediately.'
|
? 'Cancel this running ' + job.kind + ' job?\n\nThe worker may run a few seconds longer in the background, but its result will be discarded and the queue slot frees up immediately.'
|
||||||
: 'Remove this ' + job.status + ' ' + job.kind + ' job from the queue?';
|
: 'Remove this ' + job.status + ' ' + job.kind + ' job from the queue?';
|
||||||
if (!window.confirm(msg)) return;
|
if (!(await confirm({
|
||||||
|
title: mode === 'cancel' ? 'Cancel job?' : 'Remove job?',
|
||||||
|
message: msg,
|
||||||
|
confirmLabel: mode === 'cancel' ? 'Cancel job' : 'Remove',
|
||||||
|
}))) return;
|
||||||
window.ZAMPP_API.fetch('/jobs/' + job.id, { method: 'DELETE' })
|
window.ZAMPP_API.fetch('/jobs/' + job.id, { method: 'DELETE' })
|
||||||
.then(() => setJobs(prev => prev.filter(j => j.id !== job.id)))
|
.then(() => setJobs(prev => prev.filter(j => j.id !== job.id)))
|
||||||
.catch(e => alert((mode === 'cancel' ? 'Cancel' : 'Delete') + ' failed: ' + e.message));
|
.catch(e => alert((mode === 'cancel' ? 'Cancel' : 'Delete') + ' failed: ' + e.message));
|
||||||
}, []);
|
}, [confirm]);
|
||||||
|
|
||||||
// Retry every failed job at once. Useful after a transient infra issue
|
// Retry every failed job at once. Useful after a transient infra issue
|
||||||
// (S3 outage, hung worker) - one click per job is painful with 20+ failures.
|
// (S3 outage, hung worker) - one click per job is painful with 20+ failures.
|
||||||
const handleRetryAll = React.useCallback(() => {
|
const handleRetryAll = React.useCallback(async () => {
|
||||||
const failedJobs = jobs.filter(j => j.status === 'failed');
|
const failedJobs = jobs.filter(j => j.status === 'failed');
|
||||||
if (failedJobs.length === 0) return;
|
if (failedJobs.length === 0) return;
|
||||||
if (!window.confirm(`Re-queue all ${failedJobs.length} failed jobs?`)) return;
|
if (!(await confirm({
|
||||||
|
title: 'Re-queue failed jobs?',
|
||||||
|
message: `Re-queue all ${failedJobs.length} failed jobs?`,
|
||||||
|
confirmLabel: 'Re-queue',
|
||||||
|
danger: false,
|
||||||
|
}))) return;
|
||||||
Promise.allSettled(
|
Promise.allSettled(
|
||||||
failedJobs.map(j => window.ZAMPP_API.fetch('/jobs/' + j.id + '/retry', { method: 'POST' }))
|
failedJobs.map(j => window.ZAMPP_API.fetch('/jobs/' + j.id + '/retry', { method: 'POST' }))
|
||||||
).then(refresh);
|
).then(refresh);
|
||||||
}, [jobs, refresh]);
|
}, [jobs, refresh, confirm]);
|
||||||
|
|
||||||
// Drop every failed job from the queue. The opposite of Retry all — used
|
// Drop every failed job from the queue. The opposite of Retry all — used
|
||||||
// when a batch of jobs is unrecoverable (e.g. assets that were deleted
|
// when a batch of jobs is unrecoverable (e.g. assets that were deleted
|
||||||
// mid-encode) and the operator just wants the queue cleared.
|
// mid-encode) and the operator just wants the queue cleared.
|
||||||
const handleCancelAll = React.useCallback(() => {
|
const handleCancelAll = React.useCallback(async () => {
|
||||||
const failedJobs = jobs.filter(j => j.status === 'failed');
|
const failedJobs = jobs.filter(j => j.status === 'failed');
|
||||||
if (failedJobs.length === 0) return;
|
if (failedJobs.length === 0) return;
|
||||||
if (!window.confirm(`Remove all ${failedJobs.length} failed jobs from the queue?\nThis cannot be undone.`)) return;
|
if (!(await confirm({
|
||||||
|
title: 'Remove all failed jobs?',
|
||||||
|
message: `Remove all ${failedJobs.length} failed jobs from the queue?\nThis cannot be undone.`,
|
||||||
|
confirmLabel: 'Remove all',
|
||||||
|
}))) return;
|
||||||
Promise.allSettled(
|
Promise.allSettled(
|
||||||
failedJobs.map(j => window.ZAMPP_API.fetch('/jobs/' + j.id, { method: 'DELETE' }))
|
failedJobs.map(j => window.ZAMPP_API.fetch('/jobs/' + j.id, { method: 'DELETE' }))
|
||||||
).then(() => {
|
).then(() => {
|
||||||
|
|
@ -123,7 +137,7 @@ function Jobs({ navigate }) {
|
||||||
setJobs(prev => prev.filter(j => j.status !== 'failed'));
|
setJobs(prev => prev.filter(j => j.status !== 'failed'));
|
||||||
refresh();
|
refresh();
|
||||||
});
|
});
|
||||||
}, [jobs, refresh]);
|
}, [jobs, refresh, confirm]);
|
||||||
|
|
||||||
const counts = {
|
const counts = {
|
||||||
all: jobs.length,
|
all: jobs.length,
|
||||||
|
|
@ -136,6 +150,7 @@ function Jobs({ navigate }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page">
|
<div className="page">
|
||||||
|
{confirmModal}
|
||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
<h1>Jobs</h1>
|
<h1>Jobs</h1>
|
||||||
<span className="subtitle">Proxy generation, transcoding, and processing queue</span>
|
<span className="subtitle">Proxy generation, transcoding, and processing queue</span>
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ function Library({ navigate, onOpenAsset, openProject, onClearProject }) {
|
||||||
const [allAssets, setAllAssets] = React.useState(window.ZAMPP_DATA.ASSETS || []);
|
const [allAssets, setAllAssets] = React.useState(window.ZAMPP_DATA.ASSETS || []);
|
||||||
const [ctxMenu, setCtxMenu] = React.useState(null); // { asset, x, y }
|
const [ctxMenu, setCtxMenu] = React.useState(null); // { asset, x, y }
|
||||||
const [renamingAsset, setRenamingAsset] = React.useState(null);
|
const [renamingAsset, setRenamingAsset] = React.useState(null);
|
||||||
|
const [confirm, confirmModal] = window.useConfirm();
|
||||||
// Asset queued for hi-res download. Null means no modal showing. Set when
|
// Asset queued for hi-res download. Null means no modal showing. Set when
|
||||||
// the user clicks Download and has NOT dismissed the "are you sure" warning.
|
// the user clicks Download and has NOT dismissed the "are you sure" warning.
|
||||||
const [pendingDownload, setPendingDownload] = React.useState(null);
|
const [pendingDownload, setPendingDownload] = React.useState(null);
|
||||||
|
|
@ -76,6 +77,9 @@ function Library({ navigate, onOpenAsset, openProject, onClearProject }) {
|
||||||
// Rename project state
|
// Rename project state
|
||||||
const [renamingProject, setRenamingProject] = React.useState(null);
|
const [renamingProject, setRenamingProject] = React.useState(null);
|
||||||
const [projVersion, setProjVersion] = React.useState(0);
|
const [projVersion, setProjVersion] = React.useState(0);
|
||||||
|
// Multi-select state
|
||||||
|
const [selectedAssets, setSelectedAssets] = React.useState(new Set());
|
||||||
|
const [selectionMode, setSelectionMode] = React.useState(false);
|
||||||
|
|
||||||
const refreshAssets = React.useCallback(() => {
|
const refreshAssets = React.useCallback(() => {
|
||||||
window.ZAMPP_API.refreshAssets()
|
window.ZAMPP_API.refreshAssets()
|
||||||
|
|
@ -85,6 +89,76 @@ function Library({ navigate, onOpenAsset, openProject, onClearProject }) {
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const toggleSelection = function(assetId, e) {
|
||||||
|
if (e) e.stopPropagation();
|
||||||
|
setSelectedAssets(function(prev) {
|
||||||
|
var next = new Set(prev);
|
||||||
|
if (next.has(assetId)) next.delete(assetId);
|
||||||
|
else next.add(assetId);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectAll = function() {
|
||||||
|
setSelectedAssets(new Set(assets.map(function(a) { return a.id; })));
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearSelection = function() {
|
||||||
|
setSelectedAssets(new Set());
|
||||||
|
setSelectionMode(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const bulkMoveToBin = async function(binId) {
|
||||||
|
var ids = Array.from(selectedAssets);
|
||||||
|
if (ids.length === 0) return;
|
||||||
|
var targetBin = bins.find(function(b) { return b.id === binId; });
|
||||||
|
for (var i = 0; i < ids.length; i++) {
|
||||||
|
var asset = allAssets.find(function(a) { return a.id === ids[i]; });
|
||||||
|
if (asset && targetBin && asset.project_id && targetBin.project_id && asset.project_id !== targetBin.project_id) {
|
||||||
|
alert('Cannot move assets to a bin in a different project.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await Promise.all(ids.map(function(id) {
|
||||||
|
return window.ZAMPP_API.fetch('/assets/' + id, { method: 'PATCH', body: JSON.stringify({ bin_id: binId }) });
|
||||||
|
}));
|
||||||
|
refreshAssets();
|
||||||
|
window.dispatchEvent(new Event('df:bins-changed'));
|
||||||
|
clearSelection();
|
||||||
|
} catch (e) {
|
||||||
|
alert('Bulk move failed: ' + e.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const bulkDelete = async function() {
|
||||||
|
var ids = Array.from(selectedAssets);
|
||||||
|
if (ids.length === 0) return;
|
||||||
|
if (!(await confirm({
|
||||||
|
title: 'Delete ' + ids.length + ' assets?',
|
||||||
|
message: 'Delete ' + ids.length + ' assets permanently?\nThis removes the database rows and S3 objects.\nThis cannot be undone.',
|
||||||
|
}))) return;
|
||||||
|
try {
|
||||||
|
await Promise.all(ids.map(function(id) {
|
||||||
|
return window.ZAMPP_API.fetch('/assets/' + id + '?hard=true', { method: 'DELETE' });
|
||||||
|
}));
|
||||||
|
refreshAssets();
|
||||||
|
clearSelection();
|
||||||
|
} catch (e) {
|
||||||
|
alert('Bulk delete failed: ' + e.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteAsset = React.useCallback(async (asset) => {
|
||||||
|
if (!(await confirm({
|
||||||
|
title: 'Delete asset?',
|
||||||
|
message: 'Delete "' + (asset.display_name || asset.name) + '" permanently?\nThis removes the database row and the S3 objects.\nThis cannot be undone.',
|
||||||
|
}))) return;
|
||||||
|
window.ZAMPP_API.fetch('/assets/' + asset.id + '?hard=true', { method: 'DELETE' })
|
||||||
|
.then(refreshAssets)
|
||||||
|
.catch(function(e) { alert('Delete failed: ' + e.message); });
|
||||||
|
}, [confirm, refreshAssets]);
|
||||||
|
|
||||||
// Auto-refresh: poll the library while it's open so live recordings flip
|
// Auto-refresh: poll the library while it's open so live recordings flip
|
||||||
// to 'ready' (with thumbnail) without a manual reload. Also pull once on
|
// to 'ready' (with thumbnail) without a manual reload. Also pull once on
|
||||||
// mount so uploads/imports created on other screens appear immediately.
|
// mount so uploads/imports created on other screens appear immediately.
|
||||||
|
|
@ -222,6 +296,7 @@ function Library({ navigate, onOpenAsset, openProject, onClearProject }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="library-layout">
|
<div className="library-layout">
|
||||||
|
{confirmModal}
|
||||||
<aside className="library-rail">
|
<aside className="library-rail">
|
||||||
<div>
|
<div>
|
||||||
<h4>Projects</h4>
|
<h4>Projects</h4>
|
||||||
|
|
@ -310,6 +385,29 @@ function Library({ navigate, onOpenAsset, openProject, onClearProject }) {
|
||||||
<h1 className="toolbar-title">{displayTitle}</h1>
|
<h1 className="toolbar-title">{displayTitle}</h1>
|
||||||
<span className="count">· {assets.length} assets</span>
|
<span className="count">· {assets.length} assets</span>
|
||||||
<div style={{ flex: 1 }} />
|
<div style={{ flex: 1 }} />
|
||||||
|
{selectionMode && selectedAssets.size > 0 && (
|
||||||
|
<div style={{ display: 'flex', gap: 8, alignItems: 'center', marginRight: 12 }}>
|
||||||
|
<span style={{ fontSize: 13, color: 'var(--text-2)' }}>{selectedAssets.size} selected</span>
|
||||||
|
<div className="tab-group">
|
||||||
|
<button onClick={selectAll} title="Select all"><Icon name="check" size={12} /></button>
|
||||||
|
<button onClick={clearSelection} title="Clear selection"><Icon name="x" size={12} /></button>
|
||||||
|
</div>
|
||||||
|
{BINS.length > 0 && (
|
||||||
|
<select className="field-input" style={{ height: 32, fontSize: 12, padding: '0 8px' }}
|
||||||
|
onChange={function(e) { if (e.target.value) bulkMoveToBin(e.target.value); e.target.value = ''; }}
|
||||||
|
value="">
|
||||||
|
<option value="">Move to bin…</option>
|
||||||
|
{BINS.filter(function(b) { return !openProject || b.project_id === openProject.id; }).map(function(b) {
|
||||||
|
return <option key={b.id} value={b.id}>{b.name}</option>;
|
||||||
|
})}
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
|
<button className="btn danger sm" onClick={bulkDelete}><Icon name="trash" size={12} />Delete</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<button className={'btn ghost sm' + (selectionMode ? ' active' : '')} onClick={function() { setSelectionMode(!selectionMode); if (selectionMode) clearSelection(); }} title="Select multiple">
|
||||||
|
<Icon name="check" size={12} />Select
|
||||||
|
</button>
|
||||||
<div className="search" style={{ width: 220 }}>
|
<div className="search" style={{ width: 220 }}>
|
||||||
<Icon name="search" className="search-icon" />
|
<Icon name="search" className="search-icon" />
|
||||||
<input value={search} onChange={function(e) { setSearch(e.target.value); }} placeholder="Filter assets…" />
|
<input value={search} onChange={function(e) { setSearch(e.target.value); }} placeholder="Filter assets…" />
|
||||||
|
|
@ -340,18 +438,27 @@ function Library({ navigate, onOpenAsset, openProject, onClearProject }) {
|
||||||
onContextMenu={function(e) { openCtx(a, e); }}
|
onContextMenu={function(e) { openCtx(a, e); }}
|
||||||
onDownload={function() { requestDownload(a); }}
|
onDownload={function() { requestDownload(a); }}
|
||||||
onDragStart={function(e) { onAssetDragStart(a.id, e); }}
|
onDragStart={function(e) { onAssetDragStart(a.id, e); }}
|
||||||
draggable={true} />;
|
draggable={true}
|
||||||
|
selectionMode={selectionMode}
|
||||||
|
isSelected={selectedAssets.has(a.id)}
|
||||||
|
onToggleSelect={function(e) { toggleSelection(a.id, e); }} />;
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="library-list">
|
<div className="library-list">
|
||||||
<div className="list-row head">
|
<div className="list-row head">
|
||||||
|
{selectionMode && <div style={{ width: 32 }}></div>}
|
||||||
<div></div><div>Name</div><div>Duration</div><div>Resolution</div><div>Codec</div><div>Size</div><div>Updated</div><div></div>
|
<div></div><div>Name</div><div>Duration</div><div>Resolution</div><div>Codec</div><div>Size</div><div>Updated</div><div></div>
|
||||||
</div>
|
</div>
|
||||||
{assets.map(function(a) {
|
{assets.map(function(a) {
|
||||||
return (
|
return (
|
||||||
<div key={a.id} className="list-row" onClick={function() { onOpenAsset(a); }} onContextMenu={function(e) { openCtx(a, e); }} style={{ cursor: 'pointer' }}
|
<div key={a.id} className="list-row" onClick={function() { if (!selectionMode) onOpenAsset(a); }} onContextMenu={function(e) { openCtx(a, e); }} style={{ cursor: 'pointer' }}
|
||||||
draggable="true" onDragStart={function(e) { onAssetDragStart(a.id, e); }}>
|
draggable="true" onDragStart={function(e) { onAssetDragStart(a.id, e); }}>
|
||||||
|
{selectionMode && (
|
||||||
|
<div style={{ width: 32, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||||
|
<input type="checkbox" checked={selectedAssets.has(a.id)} onChange={function(e) { toggleSelection(a.id, e); }} onClick={function(e) { e.stopPropagation(); }} style={{ cursor: 'pointer' }} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="thumb"><AssetThumb asset={a} /></div>
|
<div className="thumb"><AssetThumb asset={a} /></div>
|
||||||
<div>
|
<div>
|
||||||
<div className="name">{a.name}</div>
|
<div className="name">{a.name}</div>
|
||||||
|
|
@ -383,6 +490,7 @@ function Library({ navigate, onOpenAsset, openProject, onClearProject }) {
|
||||||
onOpen={function() { onOpenAsset(ctxMenu.asset); }}
|
onOpen={function() { onOpenAsset(ctxMenu.asset); }}
|
||||||
onRename={function(a) { setCtxMenu(null); setRenamingAsset(a); }}
|
onRename={function(a) { setCtxMenu(null); setRenamingAsset(a); }}
|
||||||
onDownload={function(a) { setCtxMenu(null); requestDownload(a); }}
|
onDownload={function(a) { setCtxMenu(null); requestDownload(a); }}
|
||||||
|
onDelete={function(a) { setCtxMenu(null); deleteAsset(a); }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{pendingDownload && (
|
{pendingDownload && (
|
||||||
|
|
@ -466,7 +574,7 @@ function runDownload(asset) {
|
||||||
.catch(function(e) { alert('Download failed: ' + (e.message || 'unknown error')); });
|
.catch(function(e) { alert('Download failed: ' + (e.message || 'unknown error')); });
|
||||||
}
|
}
|
||||||
|
|
||||||
function AssetContextMenu({ asset, x, y, bins, onClose, onChanged, onOpen, onRename, onDownload }) {
|
function AssetContextMenu({ asset, x, y, bins, onClose, onChanged, onOpen, onRename, onDownload, onDelete }) {
|
||||||
const ref = React.useRef(null);
|
const ref = React.useRef(null);
|
||||||
// Pin the menu inside the viewport even if the user right-clicked near
|
// Pin the menu inside the viewport even if the user right-clicked near
|
||||||
// the bottom-right edge of the grid.
|
// the bottom-right edge of the grid.
|
||||||
|
|
@ -496,11 +604,8 @@ function AssetContextMenu({ asset, x, y, bins, onClose, onChanged, onOpen, onRen
|
||||||
};
|
};
|
||||||
|
|
||||||
const remove = function() {
|
const remove = function() {
|
||||||
|
if (onDelete) { onDelete(asset); return; }
|
||||||
onClose();
|
onClose();
|
||||||
if (!confirm('Delete "' + (asset.display_name || asset.name) + '" permanently?\nThis removes the database row and the S3 objects.\nThis cannot be undone.')) return;
|
|
||||||
window.ZAMPP_API.fetch('/assets/' + asset.id + '?hard=true', { method: 'DELETE' })
|
|
||||||
.then(onChanged)
|
|
||||||
.catch(function(e) { alert('Delete failed: ' + e.message); });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -545,7 +650,7 @@ function AssetContextMenu({ asset, x, y, bins, onClose, onChanged, onOpen, onRen
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AssetCard({ asset, onOpen, onContextMenu, onDownload, onDragStart, draggable }) {
|
function AssetCard({ asset, onOpen, onContextMenu, onDownload, onDragStart, draggable, selectionMode, isSelected, onToggleSelect }) {
|
||||||
const [hoverStream, setHoverStream] = React.useState(null);
|
const [hoverStream, setHoverStream] = React.useState(null);
|
||||||
const [hovered, setHovered] = React.useState(false);
|
const [hovered, setHovered] = React.useState(false);
|
||||||
const timerRef = React.useRef(null);
|
const timerRef = React.useRef(null);
|
||||||
|
|
@ -583,9 +688,14 @@ function AssetCard({ asset, onOpen, onContextMenu, onDownload, onDragStart, drag
|
||||||
const showVideo = hovered && hoverStream;
|
const showVideo = hovered && hoverStream;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="asset-card" onClick={onOpen} onContextMenu={onContextMenu} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}
|
<div className="asset-card" onClick={selectionMode ? onToggleSelect : onOpen} onContextMenu={onContextMenu} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}
|
||||||
draggable={draggable} onDragStart={onDragStart}>
|
draggable={draggable} onDragStart={onDragStart}>
|
||||||
<div style={{ position: 'relative' }}>
|
<div style={{ position: 'relative' }}>
|
||||||
|
{selectionMode && (
|
||||||
|
<div style={{ position: 'absolute', top: 8, left: 8, zIndex: 10 }}>
|
||||||
|
<input type="checkbox" checked={isSelected} onChange={onToggleSelect} onClick={function(e) { e.stopPropagation(); }} style={{ cursor: 'pointer', width: 18, height: 18 }} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<AssetThumb asset={asset} />
|
<AssetThumb asset={asset} />
|
||||||
{showVideo && (
|
{showVideo && (
|
||||||
<video
|
<video
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,25 @@
|
||||||
// screens-playout.jsx — Master Control (MCR) playout page.
|
// screens-playout.jsx — Master Control (MCR) playout page.
|
||||||
//
|
//
|
||||||
// Redesigned: combines the visual design (timeline, styled monitor, SCTE-35
|
// Redesigned (timeline-centric): styled PGM monitor with audio meters +
|
||||||
// panel, as-run drawer, transport bar, channel header tabs) with the full
|
// timecode, transport bar, SCTE-35 break panel, now-playing + up-next, a
|
||||||
// real API wiring from the live version.
|
// horizontal timeline, and a slide-in as-run drawer — all wired to the real
|
||||||
|
// /api/v1/playout backend (no mock data).
|
||||||
//
|
//
|
||||||
// API wiring summary:
|
// API wiring summary:
|
||||||
// - Channel CRUD: GET/POST/DELETE /playout/channels
|
// - Channel CRUD: GET/POST/DELETE /playout/channels
|
||||||
// - Channel lifecycle: POST /channels/:id/start|stop
|
// - Channel lifecycle: POST /channels/:id/start|stop
|
||||||
// - Engine status: GET /channels/:id/status (polls 4s)
|
// - Engine status: GET /channels/:id/status (polls 4s; carries scteActive)
|
||||||
// - HLS preview: GET /channels/:id/hls/index.m3u8
|
// - HLS preview: GET /channels/:id/hls/index.m3u8 (hls.js + error recovery)
|
||||||
// - Playlist CRUD: GET/POST /playlists, GET/POST/PUT /playlists/:id/items
|
// - Playlist CRUD: GET/POST /playlists, GET/POST/PUT /playlists/:id/items
|
||||||
// - Transport: POST /channels/:id/play|pause|resume|skip|stop-playback
|
// - Transport: POST /channels/:id/play|pause|resume|skip|stop-playback
|
||||||
// - As-run log: GET /channels/:id/asrun (polls 5s, drawer)
|
// - As-run log: GET /channels/:id/asrun (polls 5s, drawer)
|
||||||
// - Staging: POST /items/:id/stage (retry)
|
// - Staging: POST /items/:id/stage (retry)
|
||||||
// - SCTE-35: UI only — no backend endpoint yet (see comment below)
|
// - SCTE-35: GET/POST /channels/:id/scte, POST /channels/:id/scte/trigger,
|
||||||
|
// DELETE /channels/:id/scte/:id
|
||||||
//
|
//
|
||||||
// esbuild bundle:false + jsx:transform — no import statements.
|
// esbuild bundle:false + jsx:transform — no import statements.
|
||||||
// Globals: React, Icon, window.ZAMPP_API, window.ZAMPP_DATA, window.Hls
|
// Globals: React, Icon, window.ZAMPP_API, window.ZAMPP_DATA, window.Hls,
|
||||||
|
// window.useConfirm/ConfirmModal.
|
||||||
|
|
||||||
const PO_OUTPUTS = [
|
const PO_OUTPUTS = [
|
||||||
{ value: 'srt', label: 'SRT' },
|
{ value: 'srt', label: 'SRT' },
|
||||||
|
|
@ -33,7 +36,7 @@ async function poFetch(path, opts) {
|
||||||
|
|
||||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function fmtDuration(secs) {
|
function playoutFmtDur(secs) {
|
||||||
if (!secs || secs < 0) return '—';
|
if (!secs || secs < 0) return '—';
|
||||||
const s = Math.floor(secs);
|
const s = Math.floor(secs);
|
||||||
const h = Math.floor(s / 3600);
|
const h = Math.floor(s / 3600);
|
||||||
|
|
@ -44,7 +47,7 @@ function fmtDuration(secs) {
|
||||||
return h > 0 ? `${h}:${mm}:${ssStr}` : `${m}:${ssStr}`;
|
return h > 0 ? `${h}:${mm}:${ssStr}` : `${m}:${ssStr}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function fmtTimecode(secs) {
|
function playoutFmtTC(secs) {
|
||||||
// HH:MM:SS:FF at 29.97 (rounded)
|
// HH:MM:SS:FF at 29.97 (rounded)
|
||||||
const s = Math.floor(secs);
|
const s = Math.floor(secs);
|
||||||
const h = Math.floor(s / 3600);
|
const h = Math.floor(s / 3600);
|
||||||
|
|
@ -69,11 +72,8 @@ function LiveClock() {
|
||||||
return () => clearInterval(id);
|
return () => clearInterval(id);
|
||||||
}, []);
|
}, []);
|
||||||
const pad = n => String(n).padStart(2, '0');
|
const pad = n => String(n).padStart(2, '0');
|
||||||
const h = pad(time.getHours());
|
|
||||||
const m = pad(time.getMinutes());
|
|
||||||
const s = pad(time.getSeconds());
|
|
||||||
return (
|
return (
|
||||||
<span className="po-clock mono">{h}:{m}:{s}</span>
|
<span className="po-clock mono">{pad(time.getHours())}:{pad(time.getMinutes())}:{pad(time.getSeconds())}</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -317,7 +317,7 @@ function Playlist({ channel, playlistId, items, activeIndex, onReload }) {
|
||||||
<div className="po-playlist-head">
|
<div className="po-playlist-head">
|
||||||
<span className="po-section-label">Playlist</span>
|
<span className="po-section-label">Playlist</span>
|
||||||
<span className="mono muted" style={{ fontSize: 11, marginLeft: 'auto' }}>
|
<span className="mono muted" style={{ fontSize: 11, marginLeft: 'auto' }}>
|
||||||
{items.length} clip{items.length !== 1 ? 's' : ''} · {fmtDuration(totalSecs)}
|
{items.length} clip{items.length !== 1 ? 's' : ''} · {playoutFmtDur(totalSecs)}
|
||||||
</span>
|
</span>
|
||||||
{dropErr && <span className="po-drop-err">{dropErr}</span>}
|
{dropErr && <span className="po-drop-err">{dropErr}</span>}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -338,7 +338,7 @@ function Playlist({ channel, playlistId, items, activeIndex, onReload }) {
|
||||||
{isActive ? <span className="po-pl-onair">▶</span> : index + 1}
|
{isActive ? <span className="po-pl-onair">▶</span> : index + 1}
|
||||||
</span>
|
</span>
|
||||||
<span className="po-pl-name">{it.clip_name || it.asset_id}</span>
|
<span className="po-pl-name">{it.clip_name || it.asset_id}</span>
|
||||||
<span className="mono po-pl-dur">{fmtDuration(dur)}</span>
|
<span className="mono po-pl-dur">{playoutFmtDur(dur)}</span>
|
||||||
<span className={'badge po-pl-badge ' + (
|
<span className={'badge po-pl-badge ' + (
|
||||||
it.media_status === 'ready' ? 'success' :
|
it.media_status === 'ready' ? 'success' :
|
||||||
it.media_status === 'staging' ? 'warn' :
|
it.media_status === 'staging' ? 'warn' :
|
||||||
|
|
@ -359,8 +359,9 @@ function Playlist({ channel, playlistId, items, activeIndex, onReload }) {
|
||||||
|
|
||||||
// ── Audio meter ───────────────────────────────────────────────────────────────
|
// ── Audio meter ───────────────────────────────────────────────────────────────
|
||||||
// Simulated VU meter — real values would require a WebAudio analyzer on the
|
// Simulated VU meter — real values would require a WebAudio analyzer on the
|
||||||
// HLS stream. For now, animate a plausible signal when on-air.
|
// HLS stream. For now, animate a plausible signal when on-air. (Named PoAudioMeter
|
||||||
function AudioMeter({ onAir }) {
|
// to avoid colliding with the global AudioMeter from visuals.jsx.)
|
||||||
|
function PoAudioMeter({ onAir }) {
|
||||||
const canvasRef = React.useRef(null);
|
const canvasRef = React.useRef(null);
|
||||||
const rafRef = React.useRef(null);
|
const rafRef = React.useRef(null);
|
||||||
|
|
||||||
|
|
@ -379,7 +380,6 @@ function AudioMeter({ onAir }) {
|
||||||
ctx.clearRect(0, 0, w, h);
|
ctx.clearRect(0, 0, w, h);
|
||||||
|
|
||||||
if (onAir) {
|
if (onAir) {
|
||||||
// Simulate audio levels with a bit of randomness
|
|
||||||
const target = 0.55 + Math.sin(frame * 0.07) * 0.25 + (Math.random() - 0.5) * 0.15;
|
const target = 0.55 + Math.sin(frame * 0.07) * 0.25 + (Math.random() - 0.5) * 0.15;
|
||||||
levelL += (target - levelL) * 0.25;
|
levelL += (target - levelL) * 0.25;
|
||||||
levelR += ((target + (Math.random() - 0.5) * 0.08) - levelR) * 0.25;
|
levelR += ((target + (Math.random() - 0.5) * 0.08) - levelR) * 0.25;
|
||||||
|
|
@ -387,10 +387,8 @@ function AudioMeter({ onAir }) {
|
||||||
levelR = Math.max(0, Math.min(1, levelR));
|
levelR = Math.max(0, Math.min(1, levelR));
|
||||||
peakL = Math.max(peakL * 0.995, levelL);
|
peakL = Math.max(peakL * 0.995, levelL);
|
||||||
peakR = Math.max(peakR * 0.995, levelR);
|
peakR = Math.max(peakR * 0.995, levelR);
|
||||||
if (levelL >= peakHoldL) { peakHoldL = levelL; }
|
if (levelL >= peakHoldL) { peakHoldL = levelL; } else { peakHoldL *= 0.992; }
|
||||||
else { peakHoldL *= 0.992; }
|
if (levelR >= peakHoldR) { peakHoldR = levelR; } else { peakHoldR *= 0.992; }
|
||||||
if (levelR >= peakHoldR) { peakHoldR = levelR; }
|
|
||||||
else { peakHoldR *= 0.992; }
|
|
||||||
} else {
|
} else {
|
||||||
levelL *= 0.9; levelR *= 0.9;
|
levelL *= 0.9; levelR *= 0.9;
|
||||||
peakHoldL *= 0.9; peakHoldR *= 0.9;
|
peakHoldL *= 0.9; peakHoldR *= 0.9;
|
||||||
|
|
@ -399,26 +397,21 @@ function AudioMeter({ onAir }) {
|
||||||
const barW = Math.floor((w - 6) / 2);
|
const barW = Math.floor((w - 6) / 2);
|
||||||
const drawBar = (x, level, peakHold) => {
|
const drawBar = (x, level, peakHold) => {
|
||||||
const fillH = Math.floor(level * h);
|
const fillH = Math.floor(level * h);
|
||||||
// Background
|
|
||||||
ctx.fillStyle = 'rgba(255,255,255,0.05)';
|
ctx.fillStyle = 'rgba(255,255,255,0.05)';
|
||||||
ctx.fillRect(x, 0, barW, h);
|
ctx.fillRect(x, 0, barW, h);
|
||||||
// Green zone (bottom 70%)
|
|
||||||
const greenH = Math.min(fillH, Math.floor(h * 0.7));
|
const greenH = Math.min(fillH, Math.floor(h * 0.7));
|
||||||
ctx.fillStyle = '#22c55e';
|
ctx.fillStyle = '#22c55e';
|
||||||
ctx.fillRect(x, h - greenH, barW, greenH);
|
ctx.fillRect(x, h - greenH, barW, greenH);
|
||||||
// Yellow zone (70-90%)
|
|
||||||
if (fillH > Math.floor(h * 0.7)) {
|
if (fillH > Math.floor(h * 0.7)) {
|
||||||
const yw = Math.min(fillH - Math.floor(h * 0.7), Math.floor(h * 0.2));
|
const yw = Math.min(fillH - Math.floor(h * 0.7), Math.floor(h * 0.2));
|
||||||
ctx.fillStyle = '#eab308';
|
ctx.fillStyle = '#eab308';
|
||||||
ctx.fillRect(x, h - Math.floor(h * 0.7) - yw, barW, yw);
|
ctx.fillRect(x, h - Math.floor(h * 0.7) - yw, barW, yw);
|
||||||
}
|
}
|
||||||
// Red zone (top 10%)
|
|
||||||
if (fillH > Math.floor(h * 0.9)) {
|
if (fillH > Math.floor(h * 0.9)) {
|
||||||
const rw = fillH - Math.floor(h * 0.9);
|
const rw = fillH - Math.floor(h * 0.9);
|
||||||
ctx.fillStyle = '#ef4444';
|
ctx.fillStyle = '#ef4444';
|
||||||
ctx.fillRect(x, h - Math.floor(h * 0.9) - rw, barW, rw);
|
ctx.fillRect(x, h - Math.floor(h * 0.9) - rw, barW, rw);
|
||||||
}
|
}
|
||||||
// Peak hold
|
|
||||||
const peakY = Math.floor((1 - peakHold) * h);
|
const peakY = Math.floor((1 - peakHold) * h);
|
||||||
ctx.fillStyle = peakHold > 0.9 ? '#ef4444' : peakHold > 0.7 ? '#eab308' : '#22c55e';
|
ctx.fillStyle = peakHold > 0.9 ? '#ef4444' : peakHold > 0.7 ? '#eab308' : '#22c55e';
|
||||||
ctx.fillRect(x, peakY, barW, 2);
|
ctx.fillRect(x, peakY, barW, 2);
|
||||||
|
|
@ -444,11 +437,18 @@ function ProgramMonitor({ channel, engine, elapsed }) {
|
||||||
const videoRef = React.useRef(null);
|
const videoRef = React.useRef(null);
|
||||||
const hlsRef = React.useRef(null);
|
const hlsRef = React.useRef(null);
|
||||||
const onAir = channel.status === 'running';
|
const onAir = channel.status === 'running';
|
||||||
|
// Load the playlist through the API (not the static /media/live path): the
|
||||||
|
// public reverse proxy caches the static .m3u8 with a multi-second TTL and
|
||||||
|
// ignores no-store, which starved hls.js's reloads of the live edge and kept
|
||||||
|
// the monitor black. /api/ isn't proxy-cached, so this always returns fresh.
|
||||||
const previewUrl = `/api/v1/playout/channels/${channel.id}/hls/index.m3u8`;
|
const previewUrl = `/api/v1/playout/channels/${channel.id}/hls/index.m3u8`;
|
||||||
|
const scte = engine && engine.scteActive;
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const vid = videoRef.current;
|
const vid = videoRef.current;
|
||||||
if (!vid) return;
|
if (!vid) return;
|
||||||
|
|
||||||
|
// Tear down any previous HLS instance before re-evaluating.
|
||||||
if (hlsRef.current) { hlsRef.current.destroy(); hlsRef.current = null; }
|
if (hlsRef.current) { hlsRef.current.destroy(); hlsRef.current = null; }
|
||||||
if (!onAir) { vid.src = ''; return; }
|
if (!onAir) { vid.src = ''; return; }
|
||||||
|
|
||||||
|
|
@ -456,12 +456,60 @@ function ProgramMonitor({ channel, engine, elapsed }) {
|
||||||
const hls = new window.Hls({
|
const hls = new window.Hls({
|
||||||
liveSyncDurationCount: 3,
|
liveSyncDurationCount: 3,
|
||||||
liveMaxLatencyDurationCount: 6,
|
liveMaxLatencyDurationCount: 6,
|
||||||
|
// Keep hls.js pinned to the live edge. The preview is a CPU-encoded
|
||||||
|
// confidence monitor whose live-edge segment may still be mid-write
|
||||||
|
// when first fetched; a small back-buffer + tolerant stall handling
|
||||||
|
// lets the player skip transient gaps instead of freezing.
|
||||||
|
backBufferLength: 8,
|
||||||
|
maxBufferLength: 10,
|
||||||
|
liveDurationInfinity: true,
|
||||||
|
highBufferWatchdogPeriod: 1,
|
||||||
|
nudgeMaxRetry: 10,
|
||||||
|
// The playlist is served from /api/ (auth-gated); send the session
|
||||||
|
// cookie so the request authenticates. Segments are static + public.
|
||||||
xhrSetup: (xhr) => { xhr.withCredentials = true; },
|
xhrSetup: (xhr) => { xhr.withCredentials = true; },
|
||||||
});
|
});
|
||||||
hlsRef.current = hls;
|
hlsRef.current = hls;
|
||||||
hls.loadSource(previewUrl);
|
hls.loadSource(previewUrl);
|
||||||
hls.attachMedia(vid);
|
hls.attachMedia(vid);
|
||||||
hls.on(window.Hls.Events.MANIFEST_PARSED, () => vid.play().catch(() => {}));
|
hls.on(window.Hls.Events.MANIFEST_PARSED, () => vid.play().catch(() => {}));
|
||||||
|
|
||||||
|
// Resilient recovery. Without this, the FIRST fatal hls.js error (a
|
||||||
|
// buffer stall on the live edge, a media/decode error, or a transient
|
||||||
|
// fragment/playlist load error against the rewinding live playlist)
|
||||||
|
// permanently halts playback and the monitor goes black — exactly the
|
||||||
|
// "flashes a frame then stays black" symptom. We distinguish error types
|
||||||
|
// and recover in place rather than tearing down.
|
||||||
|
let recoverCount = 0;
|
||||||
|
hls.on(window.Hls.Events.ERROR, (_evt, data) => {
|
||||||
|
if (!data.fatal) {
|
||||||
|
if (data.details === window.Hls.ErrorDetails.BUFFER_STALLED_ERROR) {
|
||||||
|
try { hls.startLoad(); } catch (_) {}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (data.type) {
|
||||||
|
case window.Hls.ErrorTypes.NETWORK_ERROR:
|
||||||
|
try { hls.startLoad(); } catch (_) {}
|
||||||
|
break;
|
||||||
|
case window.Hls.ErrorTypes.MEDIA_ERROR:
|
||||||
|
recoverCount += 1;
|
||||||
|
if (recoverCount <= 3) {
|
||||||
|
try { hls.recoverMediaError(); } catch (_) {}
|
||||||
|
} else {
|
||||||
|
recoverCount = 0;
|
||||||
|
try { hls.destroy(); } catch (_) {}
|
||||||
|
if (hlsRef.current === hls) hlsRef.current = null;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
try { hls.destroy(); } catch (_) {}
|
||||||
|
if (hlsRef.current === hls) hlsRef.current = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
hls.on(window.Hls.Events.FRAG_BUFFERED, () => {
|
||||||
|
if (vid.paused) vid.play().catch(() => {});
|
||||||
|
});
|
||||||
} else if (vid.canPlayType('application/vnd.apple.mpegurl')) {
|
} else if (vid.canPlayType('application/vnd.apple.mpegurl')) {
|
||||||
vid.src = previewUrl;
|
vid.src = previewUrl;
|
||||||
vid.play().catch(() => {});
|
vid.play().catch(() => {});
|
||||||
|
|
@ -478,16 +526,23 @@ function ProgramMonitor({ channel, engine, elapsed }) {
|
||||||
const progress = clipDurSecs > 0 ? Math.min(1, elapsed / clipDurSecs) : 0;
|
const progress = clipDurSecs > 0 ? Math.min(1, elapsed / clipDurSecs) : 0;
|
||||||
const timeRemaining = clipDurSecs > 0 ? Math.max(0, clipDurSecs - elapsed) : 0;
|
const timeRemaining = clipDurSecs > 0 ? Math.max(0, clipDurSecs - elapsed) : 0;
|
||||||
|
|
||||||
|
// SCTE break countdown (seconds remaining in the active break).
|
||||||
|
const breakRemain = scte && scte.endsAt
|
||||||
|
? Math.max(0, (new Date(scte.endsAt).getTime() - Date.now()) / 1000)
|
||||||
|
: 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="po-pgm">
|
<div className="po-pgm">
|
||||||
{/* Screen */}
|
|
||||||
<div className="po-screen">
|
<div className="po-screen">
|
||||||
<video ref={videoRef} className="po-monitor-video" muted playsInline autoPlay />
|
<video ref={videoRef} className="po-monitor-video" muted playsInline autoPlay />
|
||||||
|
|
||||||
{/* ON AIR badge */}
|
{/* ON AIR / SCTE BREAK badge */}
|
||||||
{onAir && (
|
{onAir && scte && (
|
||||||
<div className="po-onair-badge">ON AIR</div>
|
<div className="po-onair-badge" style={{ background: '#f59e0b' }}>
|
||||||
|
SCTE BREAK{breakRemain > 0 ? ' ' + Math.ceil(breakRemain) + 's' : ''}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{onAir && !scte && <div className="po-onair-badge">ON AIR</div>}
|
||||||
|
|
||||||
{!onAir && (
|
{!onAir && (
|
||||||
<div className="po-screen-offline">
|
<div className="po-screen-offline">
|
||||||
|
|
@ -496,14 +551,12 @@ function ProgramMonitor({ channel, engine, elapsed }) {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Timecode overlay */}
|
|
||||||
{onAir && (
|
{onAir && (
|
||||||
<div className="po-tc-overlay mono">{fmtTimecode(elapsed)}</div>
|
<div className="po-tc-overlay mono">{playoutFmtTC(elapsed)}</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Audio meters */}
|
|
||||||
<div className="po-meters-wrap">
|
<div className="po-meters-wrap">
|
||||||
<AudioMeter onAir={onAir} />
|
<PoAudioMeter onAir={onAir} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -527,7 +580,7 @@ function ProgramMonitor({ channel, engine, elapsed }) {
|
||||||
</span>
|
</span>
|
||||||
{timeRemaining > 0 && (
|
{timeRemaining > 0 && (
|
||||||
<span className="po-clip-remain" title="Time remaining in clip">
|
<span className="po-clip-remain" title="Time remaining in clip">
|
||||||
-{fmtDuration(timeRemaining)}
|
-{playoutFmtDur(timeRemaining)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{engine.loop && <span title="Loop">↺</span>}
|
{engine.loop && <span title="Loop">↺</span>}
|
||||||
|
|
@ -543,9 +596,13 @@ function ProgramMonitor({ channel, engine, elapsed }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Transport bar ─────────────────────────────────────────────────────────────
|
// ── Transport bar ─────────────────────────────────────────────────────────────
|
||||||
function Transport({ channel, playlistId, items, onStatus }) {
|
function Transport({ channel, playlistId, items, onStatus, onError }) {
|
||||||
const [busy, setBusy] = React.useState(false);
|
const [busy, setBusy] = React.useState(false);
|
||||||
const act = async (fn) => { setBusy(true); try { await fn(); } catch (e) { alert(e.message); } finally { setBusy(false); } };
|
const act = async (fn) => {
|
||||||
|
setBusy(true);
|
||||||
|
try { await fn(); } catch (e) { onError && onError(e.message); }
|
||||||
|
finally { setBusy(false); }
|
||||||
|
};
|
||||||
|
|
||||||
const notReady = items.filter(i => i.media_status !== 'ready').length;
|
const notReady = items.filter(i => i.media_status !== 'ready').length;
|
||||||
const canPlay = channel.status === 'running' && !busy && !!playlistId && notReady === 0;
|
const canPlay = channel.status === 'running' && !busy && !!playlistId && notReady === 0;
|
||||||
|
|
@ -589,64 +646,81 @@ function Transport({ channel, playlistId, items, onStatus }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── SCTE-35 panel ─────────────────────────────────────────────────────────────
|
// ── SCTE-35 panel ─────────────────────────────────────────────────────────────
|
||||||
// NOTE: No SCTE-35 endpoint exists in /api/v1/playout at this time.
|
// Wired to the real backend:
|
||||||
// The backend does not yet implement POST /channels/:id/scte35 or similar.
|
// POST /channels/:id/scte/trigger — splice now (immediate ad break)
|
||||||
// These buttons are wired to a stub that logs the intent and shows a toast.
|
// POST /channels/:id/scte — schedule a break (at a playlist pos)
|
||||||
// When the backend is ready, replace `scte35Stub` with the real poFetch call.
|
// GET /channels/:id/scte — recent breaks
|
||||||
function Scte35Panel({ channel }) {
|
// The active break (with countdown) comes from engine.scteActive on /status.
|
||||||
const [lastFired, setLastFired] = React.useState(null);
|
function Scte35Panel({ channel, engine, breaks, onReload, onError }) {
|
||||||
const [busy, setBusy] = React.useState(false);
|
const [busy, setBusy] = React.useState(false);
|
||||||
|
const live = channel.status === 'running';
|
||||||
|
const scte = engine && engine.scteActive;
|
||||||
|
|
||||||
const scte35Stub = async (type, duration) => {
|
const triggerNow = (durationS, type) => async () => {
|
||||||
// TODO: wire to POST /playout/channels/:id/scte35 when backend implements it.
|
|
||||||
// Example body: { type, duration_s: duration }
|
|
||||||
console.warn('[SCTE-35] Backend endpoint not yet implemented. Would send:', { type, duration });
|
|
||||||
setLastFired({ type, duration, ts: new Date() });
|
|
||||||
};
|
|
||||||
|
|
||||||
const fire = (type, duration) => async () => {
|
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
try { await scte35Stub(type, duration); }
|
try {
|
||||||
catch (e) { alert('SCTE-35 error: ' + e.message); }
|
await poFetch('/channels/' + channel.id + '/scte/trigger', {
|
||||||
|
method: 'POST', body: JSON.stringify({ type: type || 'immediate', duration_s: durationS }),
|
||||||
|
});
|
||||||
|
onReload && onReload();
|
||||||
|
} catch (e) { onError && onError('SCTE-35: ' + e.message); }
|
||||||
finally { setBusy(false); }
|
finally { setBusy(false); }
|
||||||
};
|
};
|
||||||
|
|
||||||
const fmt = (ts) => ts ? ts.toLocaleTimeString() : null;
|
const scheduleAfterCurrent = (durationS) => async () => {
|
||||||
|
setBusy(true);
|
||||||
|
try {
|
||||||
|
const pos = (engine && engine.currentIndex >= 0) ? engine.currentIndex : 0;
|
||||||
|
await poFetch('/channels/' + channel.id + '/scte', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ type: 'splice_insert', duration_s: durationS, playlist_pos: pos }),
|
||||||
|
});
|
||||||
|
onReload && onReload();
|
||||||
|
} catch (e) { onError && onError('SCTE-35: ' + e.message); }
|
||||||
|
finally { setBusy(false); }
|
||||||
|
};
|
||||||
|
|
||||||
|
const lastFired = (breaks || []).find(b => b.fired_at) || null;
|
||||||
|
const pending = (breaks || []).filter(b => b.status === 'pending');
|
||||||
|
const breakRemain = scte && scte.endsAt
|
||||||
|
? Math.max(0, Math.ceil((new Date(scte.endsAt).getTime() - Date.now()) / 1000))
|
||||||
|
: 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="po-card po-scte-card">
|
<div className="po-card po-scte-card">
|
||||||
<div className="po-card-head">
|
<div className="po-card-head">
|
||||||
<span className="po-section-label">SCTE-35 Break</span>
|
<span className="po-section-label">SCTE-35 Break</span>
|
||||||
<span className="po-scte-stub-badge" title="Backend endpoint not yet implemented">stub</span>
|
{scte
|
||||||
|
? <span className="po-scte-stub-badge" style={{ color: '#f59e0b' }}>● ON AIR</span>
|
||||||
|
: pending.length > 0
|
||||||
|
? <span className="po-scte-stub-badge">{pending.length} queued</span>
|
||||||
|
: null}
|
||||||
</div>
|
</div>
|
||||||
<div className="po-scte-body">
|
<div className="po-scte-body">
|
||||||
|
{scte && (
|
||||||
|
<div className="po-scte-active mono" style={{ color: '#f59e0b', fontWeight: 600, fontSize: 12 }}>
|
||||||
|
In break · {scte.type} · {breakRemain > 0 ? breakRemain + 's left' : 'awaiting return'}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="po-section-label" style={{ fontSize: 10 }}>Trigger now</div>
|
||||||
<div className="po-scte-row">
|
<div className="po-scte-row">
|
||||||
<button className="po-fire amber" disabled={busy || channel.status !== 'running'}
|
<button className="po-fire amber" disabled={busy || !live} onClick={triggerNow(30)}>30s</button>
|
||||||
onClick={fire('splice_insert', 30)}>
|
<button className="po-fire amber" disabled={busy || !live} onClick={triggerNow(60)}>60s</button>
|
||||||
30s Break
|
<button className="po-fire amber" disabled={busy || !live} onClick={triggerNow(120)}>2m</button>
|
||||||
</button>
|
|
||||||
<button className="po-fire amber" disabled={busy || channel.status !== 'running'}
|
|
||||||
onClick={fire('splice_insert', 60)}>
|
|
||||||
60s Break
|
|
||||||
</button>
|
|
||||||
<button className="po-fire amber" disabled={busy || channel.status !== 'running'}
|
|
||||||
onClick={fire('splice_insert', 120)}>
|
|
||||||
2m Break
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="po-scte-row">
|
<div className="po-scte-row">
|
||||||
<button className="po-fire in" disabled={busy || channel.status !== 'running'}
|
<button className="po-fire out" disabled={busy || !live} onClick={triggerNow(0, 'splice_out')}>Splice Out</button>
|
||||||
onClick={fire('splice_in', 0)}>
|
<button className="po-fire in" disabled={busy || !live} onClick={triggerNow(0, 'splice_in')}>Return</button>
|
||||||
Splice In
|
</div>
|
||||||
</button>
|
<div className="po-section-label" style={{ fontSize: 10, marginTop: 4 }}>Schedule after current clip</div>
|
||||||
<button className="po-fire out" disabled={busy || channel.status !== 'running'}
|
<div className="po-scte-row">
|
||||||
onClick={fire('splice_out', 0)}>
|
<button className="po-fire" disabled={busy || !live} onClick={scheduleAfterCurrent(30)}>+30s</button>
|
||||||
Splice Out
|
<button className="po-fire" disabled={busy || !live} onClick={scheduleAfterCurrent(60)}>+60s</button>
|
||||||
</button>
|
<button className="po-fire" disabled={busy || !live} onClick={scheduleAfterCurrent(120)}>+2m</button>
|
||||||
</div>
|
</div>
|
||||||
{lastFired && (
|
{lastFired && (
|
||||||
<div className="po-scte-last mono">
|
<div className="po-scte-last mono">
|
||||||
Last: {lastFired.type} {lastFired.duration > 0 ? lastFired.duration + 's' : ''} @ {fmt(lastFired.ts)}
|
Last: {lastFired.type} {lastFired.duration_s > 0 ? lastFired.duration_s + 's' : ''} (event {lastFired.event_id})
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -671,7 +745,6 @@ function NowPlayingCard({ engine, elapsed, items }) {
|
||||||
const clipDurSecs = currentItem ? itemEffectiveDuration(currentItem) : 0;
|
const clipDurSecs = currentItem ? itemEffectiveDuration(currentItem) : 0;
|
||||||
const progress = clipDurSecs > 0 ? Math.min(1, elapsed / clipDurSecs) : 0;
|
const progress = clipDurSecs > 0 ? Math.min(1, elapsed / clipDurSecs) : 0;
|
||||||
const timeRemaining = clipDurSecs > 0 ? Math.max(0, clipDurSecs - elapsed) : 0;
|
const timeRemaining = clipDurSecs > 0 ? Math.max(0, clipDurSecs - elapsed) : 0;
|
||||||
|
|
||||||
const nextItem = items[engine.currentIndex + 1] || null;
|
const nextItem = items[engine.currentIndex + 1] || null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -688,8 +761,8 @@ function NowPlayingCard({ engine, elapsed, items }) {
|
||||||
<div className="po-nowplaying-fill" style={{ width: (progress * 100) + '%' }} />
|
<div className="po-nowplaying-fill" style={{ width: (progress * 100) + '%' }} />
|
||||||
</div>
|
</div>
|
||||||
<div className="po-nowplaying-times mono">
|
<div className="po-nowplaying-times mono">
|
||||||
<span className="po-nowplaying-elapsed">{fmtDuration(elapsed)}</span>
|
<span className="po-nowplaying-elapsed">{playoutFmtDur(elapsed)}</span>
|
||||||
<span className="po-nowplaying-remain muted">-{fmtDuration(timeRemaining)}</span>
|
<span className="po-nowplaying-remain muted">-{playoutFmtDur(timeRemaining)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{nextItem && (
|
{nextItem && (
|
||||||
|
|
@ -703,9 +776,7 @@ function NowPlayingCard({ engine, elapsed, items }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Timeline ──────────────────────────────────────────────────────────────────
|
// ── Timeline ──────────────────────────────────────────────────────────────────
|
||||||
// Shows real playlist items mapped to a horizontal timeline. Width proportional
|
function Timeline({ items, activeIndex, elapsed, breaks }) {
|
||||||
// to duration. Clicking a clip is informational (no seek API on the engine).
|
|
||||||
function Timeline({ items, activeIndex, elapsed }) {
|
|
||||||
const totalSecs = items.reduce((sum, it) => sum + itemEffectiveDuration(it), 0);
|
const totalSecs = items.reduce((sum, it) => sum + itemEffectiveDuration(it), 0);
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -718,7 +789,6 @@ function Timeline({ items, activeIndex, elapsed }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute offset of active clip for the playhead
|
|
||||||
let playheadPct = 0;
|
let playheadPct = 0;
|
||||||
if (activeIndex >= 0 && totalSecs > 0) {
|
if (activeIndex >= 0 && totalSecs > 0) {
|
||||||
const offsetSecs = items.slice(0, activeIndex).reduce((s, it) => s + itemEffectiveDuration(it), 0);
|
const offsetSecs = items.slice(0, activeIndex).reduce((s, it) => s + itemEffectiveDuration(it), 0);
|
||||||
|
|
@ -726,19 +796,33 @@ function Timeline({ items, activeIndex, elapsed }) {
|
||||||
playheadPct = ((offsetSecs + Math.min(elapsed, clipDur)) / totalSecs) * 100;
|
playheadPct = ((offsetSecs + Math.min(elapsed, clipDur)) / totalSecs) * 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pending position-based breaks → markers at the end of their playlist_pos clip.
|
||||||
|
const breakMarkers = [];
|
||||||
|
if (totalSecs > 0) {
|
||||||
|
for (const b of (breaks || [])) {
|
||||||
|
if (b.status !== 'pending' || b.playlist_pos == null) continue;
|
||||||
|
const pos = Math.min(b.playlist_pos, items.length - 1);
|
||||||
|
const offsetSecs = items.slice(0, pos + 1).reduce((s, it) => s + itemEffectiveDuration(it), 0);
|
||||||
|
breakMarkers.push({ id: b.id, pct: (offsetSecs / totalSecs) * 100, dur: b.duration_s });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const COLORS = ['#3b82f6','#8b5cf6','#06b6d4','#10b981','#f59e0b','#ec4899','#6366f1','#14b8a6'];
|
const COLORS = ['#3b82f6','#8b5cf6','#06b6d4','#10b981','#f59e0b','#ec4899','#6366f1','#14b8a6'];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="po-tl">
|
<div className="po-tl">
|
||||||
<div className="po-tl-head">
|
<div className="po-tl-head">
|
||||||
<span className="po-section-label">Timeline</span>
|
<span className="po-section-label">Timeline</span>
|
||||||
<span className="mono muted" style={{ fontSize: 11 }}>{fmtDuration(totalSecs)} total</span>
|
<span className="mono muted" style={{ fontSize: 11 }}>{playoutFmtDur(totalSecs)} total</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="po-tl-track-wrap">
|
<div className="po-tl-track-wrap">
|
||||||
{/* Playhead */}
|
|
||||||
{activeIndex >= 0 && (
|
{activeIndex >= 0 && (
|
||||||
<div className="po-tl-playhead" style={{ left: playheadPct + '%' }} />
|
<div className="po-tl-playhead" style={{ left: playheadPct + '%' }} />
|
||||||
)}
|
)}
|
||||||
|
{breakMarkers.map(m => (
|
||||||
|
<div key={m.id} className="po-tl-scte-marker" style={{ left: m.pct + '%' }}
|
||||||
|
title={'SCTE-35 break · ' + m.dur + 's'} />
|
||||||
|
))}
|
||||||
<div className="po-tl-track">
|
<div className="po-tl-track">
|
||||||
{items.map((it, i) => {
|
{items.map((it, i) => {
|
||||||
const dur = itemEffectiveDuration(it);
|
const dur = itemEffectiveDuration(it);
|
||||||
|
|
@ -749,9 +833,9 @@ function Timeline({ items, activeIndex, elapsed }) {
|
||||||
<div key={it.id}
|
<div key={it.id}
|
||||||
className={'po-tl-clip' + (isActive ? ' po-tl-clip--active' : '')}
|
className={'po-tl-clip' + (isActive ? ' po-tl-clip--active' : '')}
|
||||||
style={{ width: pct + '%', '--clip-color': color }}
|
style={{ width: pct + '%', '--clip-color': color }}
|
||||||
title={`${it.clip_name || it.asset_id} · ${fmtDuration(dur)}`}>
|
title={`${it.clip_name || it.asset_id} · ${playoutFmtDur(dur)}`}>
|
||||||
<span className="po-tl-clip-name">{it.clip_name || it.asset_id}</span>
|
<span className="po-tl-clip-name">{it.clip_name || it.asset_id}</span>
|
||||||
<span className="po-tl-clip-dur mono">{fmtDuration(dur)}</span>
|
<span className="po-tl-clip-dur mono">{playoutFmtDur(dur)}</span>
|
||||||
{it.media_status === 'staging' && (
|
{it.media_status === 'staging' && (
|
||||||
<span className="po-tl-staging-dot" title="Staging…" />
|
<span className="po-tl-staging-dot" title="Staging…" />
|
||||||
)}
|
)}
|
||||||
|
|
@ -762,12 +846,10 @@ function Timeline({ items, activeIndex, elapsed }) {
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
{/* Time ruler (rough marks) */}
|
|
||||||
<div className="po-tl-ruler">
|
<div className="po-tl-ruler">
|
||||||
{totalSecs > 0 && Array.from({ length: 5 }).map((_, i) => (
|
{totalSecs > 0 && Array.from({ length: 5 }).map((_, i) => (
|
||||||
<span key={i} className="po-tl-ruler-mark mono"
|
<span key={i} className="po-tl-ruler-mark mono" style={{ left: (i * 25) + '%' }}>
|
||||||
style={{ left: (i * 25) + '%' }}>
|
{playoutFmtDur((totalSecs * i) / 4)}
|
||||||
{fmtDuration((totalSecs * i) / 4)}
|
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -781,6 +863,7 @@ function AsRunDrawer({ channel, refreshKey, open, onClose }) {
|
||||||
const [rows, setRows] = React.useState([]);
|
const [rows, setRows] = React.useState([]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
let alive = true;
|
let alive = true;
|
||||||
let t;
|
let t;
|
||||||
const poll = async () => {
|
const poll = async () => {
|
||||||
|
|
@ -792,7 +875,7 @@ function AsRunDrawer({ channel, refreshKey, open, onClose }) {
|
||||||
};
|
};
|
||||||
poll();
|
poll();
|
||||||
return () => { alive = false; clearTimeout(t); };
|
return () => { alive = false; clearTimeout(t); };
|
||||||
}, [channel.id, refreshKey]);
|
}, [channel.id, refreshKey, open]);
|
||||||
|
|
||||||
const fmtTime = (ts) => {
|
const fmtTime = (ts) => {
|
||||||
if (!ts) return '—';
|
if (!ts) return '—';
|
||||||
|
|
@ -802,7 +885,6 @@ function AsRunDrawer({ channel, refreshKey, open, onClose }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{/* Backdrop */}
|
|
||||||
{open && <div className="po-drawer-backdrop" onClick={onClose} />}
|
{open && <div className="po-drawer-backdrop" onClick={onClose} />}
|
||||||
<div className={'po-drawer' + (open ? ' po-drawer--open' : '')}>
|
<div className={'po-drawer' + (open ? ' po-drawer--open' : '')}>
|
||||||
<div className="po-drawer-head">
|
<div className="po-drawer-head">
|
||||||
|
|
@ -825,7 +907,7 @@ function AsRunDrawer({ channel, refreshKey, open, onClose }) {
|
||||||
<td>{r.clip_name || r.item_id || '—'}</td>
|
<td>{r.clip_name || r.item_id || '—'}</td>
|
||||||
<td className="mono">
|
<td className="mono">
|
||||||
{r.duration_s != null
|
{r.duration_s != null
|
||||||
? fmtDuration(Number(r.duration_s))
|
? playoutFmtDur(Number(r.duration_s))
|
||||||
: (r.ended_at ? '—' : 'on air')}
|
: (r.ended_at ? '—' : 'on air')}
|
||||||
</td>
|
</td>
|
||||||
<td className={'po-asrun-result po-asrun-' + (r.result || 'played')}>
|
<td className={'po-asrun-result po-asrun-' + (r.result || 'played')}>
|
||||||
|
|
@ -851,6 +933,9 @@ function ChannelDetail({ channel, onChannelChange }) {
|
||||||
const [ch, setCh] = React.useState(channel);
|
const [ch, setCh] = React.useState(channel);
|
||||||
const [asRunOpen, setAsRunOpen] = React.useState(false);
|
const [asRunOpen, setAsRunOpen] = React.useState(false);
|
||||||
const [binOpen, setBinOpen] = React.useState(false);
|
const [binOpen, setBinOpen] = React.useState(false);
|
||||||
|
const [breaks, setBreaks] = React.useState([]);
|
||||||
|
const [actionErr, setActionErr] = React.useState(null);
|
||||||
|
const [confirm, confirmModal] = window.useConfirm();
|
||||||
|
|
||||||
React.useEffect(() => { setCh(channel); }, [channel.id]);
|
React.useEffect(() => { setCh(channel); }, [channel.id]);
|
||||||
|
|
||||||
|
|
@ -872,10 +957,15 @@ function ChannelDetail({ channel, onChannelChange }) {
|
||||||
setItems(its);
|
setItems(its);
|
||||||
}, [playlistId]);
|
}, [playlistId]);
|
||||||
|
|
||||||
React.useEffect(() => { loadPlaylists(); }, [channel.id]);
|
const loadBreaks = React.useCallback(async () => {
|
||||||
|
try { setBreaks(await poFetch('/channels/' + channel.id + '/scte')); }
|
||||||
|
catch (_) { /* table may be empty / migration pending */ }
|
||||||
|
}, [channel.id]);
|
||||||
|
|
||||||
|
React.useEffect(() => { loadPlaylists(); loadBreaks(); }, [channel.id]);
|
||||||
React.useEffect(() => { loadItems(); }, [playlistId]);
|
React.useEffect(() => { loadItems(); }, [playlistId]);
|
||||||
|
|
||||||
// Poll engine status + item staging.
|
// Poll engine status + item staging + SCTE breaks.
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
let t;
|
let t;
|
||||||
const poll = async () => {
|
const poll = async () => {
|
||||||
|
|
@ -884,6 +974,7 @@ function ChannelDetail({ channel, onChannelChange }) {
|
||||||
setEngine(s.engine || null);
|
setEngine(s.engine || null);
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
try { await loadItems(); } catch (_) {}
|
try { await loadItems(); } catch (_) {}
|
||||||
|
try { await loadBreaks(); } catch (_) {}
|
||||||
t = setTimeout(poll, 4000);
|
t = setTimeout(poll, 4000);
|
||||||
};
|
};
|
||||||
poll();
|
poll();
|
||||||
|
|
@ -891,30 +982,38 @@ function ChannelDetail({ channel, onChannelChange }) {
|
||||||
}, [channel.id, playlistId]);
|
}, [channel.id, playlistId]);
|
||||||
|
|
||||||
const startChannel = async () => {
|
const startChannel = async () => {
|
||||||
const updated = await poFetch('/channels/' + channel.id + '/start', { method: 'POST' });
|
try {
|
||||||
setCh(updated); onChannelChange(updated);
|
const updated = await poFetch('/channels/' + channel.id + '/start', { method: 'POST' });
|
||||||
|
setCh(updated); onChannelChange(updated);
|
||||||
|
} catch (e) { setActionErr(e.message); }
|
||||||
};
|
};
|
||||||
const stopChannel = async () => {
|
const stopChannel = async () => {
|
||||||
const updated = await poFetch('/channels/' + channel.id + '/stop', { method: 'POST' });
|
try {
|
||||||
setCh(updated); onChannelChange(updated);
|
const updated = await poFetch('/channels/' + channel.id + '/stop', { method: 'POST' });
|
||||||
|
setCh(updated); onChannelChange(updated);
|
||||||
|
} catch (e) { setActionErr(e.message); }
|
||||||
};
|
};
|
||||||
const deleteChannel = async () => {
|
const deleteChannel = async () => {
|
||||||
if (!window.confirm('Delete channel "' + ch.name + '"? This cannot be undone.')) return;
|
if (!(await confirm({
|
||||||
|
title: 'Delete channel?',
|
||||||
|
message: 'Delete channel "' + ch.name + '"? This cannot be undone.',
|
||||||
|
confirmLabel: 'Delete', danger: true,
|
||||||
|
}))) return;
|
||||||
try {
|
try {
|
||||||
await poFetch('/channels/' + ch.id, { method: 'DELETE' });
|
await poFetch('/channels/' + ch.id, { method: 'DELETE' });
|
||||||
onChannelChange({ ...ch, _deleted: true });
|
onChannelChange({ ...ch, _deleted: true });
|
||||||
} catch (e) { alert(e.message); }
|
} catch (e) { setActionErr(e.message); }
|
||||||
};
|
};
|
||||||
|
|
||||||
const activeIndex = (engine && engine.currentIndex >= 0) ? engine.currentIndex : -1;
|
const activeIndex = (engine && engine.currentIndex >= 0) ? engine.currentIndex : -1;
|
||||||
const elapsed = useElapsed(engine && engine.currentItemStartedAt);
|
const elapsed = useElapsed(engine && engine.currentItemStartedAt);
|
||||||
const onAir = ch.status === 'running';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="po-root">
|
<div className="po-root">
|
||||||
|
{confirmModal}
|
||||||
|
|
||||||
{/* ── Top rail: monitor + right panel ── */}
|
{/* ── Top rail: monitor + right panel ── */}
|
||||||
<div className="po-top">
|
<div className="po-top">
|
||||||
{/* PGM monitor + transport */}
|
|
||||||
<div className="po-pgm-col">
|
<div className="po-pgm-col">
|
||||||
<ProgramMonitor channel={ch} engine={engine} elapsed={elapsed} />
|
<ProgramMonitor channel={ch} engine={engine} elapsed={elapsed} />
|
||||||
<Transport
|
<Transport
|
||||||
|
|
@ -922,10 +1021,10 @@ function ChannelDetail({ channel, onChannelChange }) {
|
||||||
playlistId={playlistId}
|
playlistId={playlistId}
|
||||||
items={items}
|
items={items}
|
||||||
onStatus={loadItems}
|
onStatus={loadItems}
|
||||||
|
onError={setActionErr}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right rail */}
|
|
||||||
<div className="po-rail">
|
<div className="po-rail">
|
||||||
{/* Channel controls */}
|
{/* Channel controls */}
|
||||||
<div className="po-card po-channel-card">
|
<div className="po-card po-channel-card">
|
||||||
|
|
@ -949,18 +1048,21 @@ function ChannelDetail({ channel, onChannelChange }) {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{ch.error_message && <div className="alert error" style={{ marginTop: 8 }}>{ch.error_message}</div>}
|
{ch.error_message && <div className="alert error" style={{ marginTop: 8 }}>{ch.error_message}</div>}
|
||||||
|
{actionErr && (
|
||||||
|
<div className="alert error" style={{ marginTop: 8 }} onClick={() => setActionErr(null)}>
|
||||||
|
{actionErr}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Now playing */}
|
|
||||||
<NowPlayingCard engine={engine} elapsed={elapsed} items={items} />
|
<NowPlayingCard engine={engine} elapsed={elapsed} items={items} />
|
||||||
|
|
||||||
{/* SCTE-35 */}
|
<Scte35Panel channel={ch} engine={engine} breaks={breaks}
|
||||||
<Scte35Panel channel={ch} />
|
onReload={loadBreaks} onError={setActionErr} />
|
||||||
|
|
||||||
{/* Quick actions */}
|
|
||||||
<div className="po-rail-actions">
|
<div className="po-rail-actions">
|
||||||
<button className="btn ghost sm po-rail-action-btn" onClick={() => setBinOpen(b => !b)}>
|
<button className="btn ghost sm po-rail-action-btn" onClick={() => setBinOpen(b => !b)}>
|
||||||
{binOpen ? '▸ Hide' : '▾ Media Bin'}
|
{binOpen ? '▸ Hide bin' : '▾ Media Bin'}
|
||||||
</button>
|
</button>
|
||||||
<button className="btn ghost sm po-rail-action-btn" onClick={() => setAsRunOpen(true)}>
|
<button className="btn ghost sm po-rail-action-btn" onClick={() => setAsRunOpen(true)}>
|
||||||
As-Run Log
|
As-Run Log
|
||||||
|
|
@ -969,12 +1071,8 @@ function ChannelDetail({ channel, onChannelChange }) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Media bin (collapsible, below top rail) */}
|
{binOpen && <MediaBin projectId={ch.project_id} />}
|
||||||
{binOpen && (
|
|
||||||
<MediaBin projectId={ch.project_id} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Playlist */}
|
|
||||||
{playlistId && (
|
{playlistId && (
|
||||||
<Playlist
|
<Playlist
|
||||||
channel={ch}
|
channel={ch}
|
||||||
|
|
@ -985,10 +1083,8 @@ function ChannelDetail({ channel, onChannelChange }) {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Timeline */}
|
<Timeline items={items} activeIndex={activeIndex} elapsed={elapsed} breaks={breaks} />
|
||||||
<Timeline items={items} activeIndex={activeIndex} elapsed={elapsed} />
|
|
||||||
|
|
||||||
{/* As-run drawer */}
|
|
||||||
<AsRunDrawer
|
<AsRunDrawer
|
||||||
channel={ch}
|
channel={ch}
|
||||||
refreshKey={engine && engine.currentItemId}
|
refreshKey={engine && engine.currentItemId}
|
||||||
|
|
@ -1051,6 +1147,21 @@ function Playout() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="page-body po-page">
|
<div className="page-body po-page">
|
||||||
|
<div style={{
|
||||||
|
background: '#fef3c7',
|
||||||
|
borderLeft: '4px solid #f59e0b',
|
||||||
|
color: '#78350f',
|
||||||
|
padding: '10px 16px',
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: 14,
|
||||||
|
borderRadius: 4,
|
||||||
|
marginBottom: 12,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 8,
|
||||||
|
}}>
|
||||||
|
⚠ Playout is in testing — not for production use.
|
||||||
|
</div>
|
||||||
{err && <div className="alert error">{err}</div>}
|
{err && <div className="alert error">{err}</div>}
|
||||||
|
|
||||||
{channels === null && <div className="muted">Loading channels…</div>}
|
{channels === null && <div className="muted">Loading channels…</div>}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue