Compare commits
78 commits
feat/user-
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| fffff1c016 | |||
| 549ca6c73f | |||
| ea615c8c76 | |||
| f2d5f5aa16 | |||
| d908c0c056 | |||
| b597ffd58e | |||
| be8fd691a5 | |||
| dba3435e60 | |||
| 43011bd794 | |||
| 19f0abeabe | |||
| f21bc490e8 | |||
| 7451d7c703 | |||
| d3ad2397fb | |||
| 24d10fda5d | |||
| 59551f28a5 | |||
| e0e0b83810 | |||
| 5968d4f681 | |||
| 3122dfd1b9 | |||
| 3b2f9fe6a0 | |||
| 869ae1aa83 | |||
| abfbd034ab | |||
| 739d08d4b5 | |||
| 27a868aa5c | |||
| 1db0e81efb | |||
| 40e987b4a2 | |||
| 426273129d | |||
| 87d988810f | |||
| f28799317d | |||
| d778aa4cdb | |||
| 00b04aa4a8 | |||
| e8f91cf4b4 | |||
| e51cf1aa9c | |||
| f7cf56ae0d | |||
| 12115a053a | |||
| 43656a5e88 | |||
| 68461af990 | |||
| 8bc460025d | |||
| 3578c7b4e9 | |||
| cddcc9a29e | |||
| 0e844c0fc3 | |||
| 551af09dc7 | |||
| 4d6a999665 | |||
| f971d57bb9 | |||
| 7ab70948a0 | |||
| 13bbd4216e | |||
| fcd8e8dd2e | |||
| 67ac007706 | |||
| b4f2fb12ff | |||
| aa7f836493 | |||
| c2409bd037 | |||
| 42064acefa | |||
| 2e2b091653 | |||
|
|
c502d4a16f | ||
|
|
9d098e9778 | ||
|
|
02631f7b96 | ||
|
|
9436434599 | ||
|
|
f837e57969 | ||
|
|
ca71e47035 | ||
|
|
34352e3299 | ||
|
|
d505a488ac | ||
|
|
793011b78b | ||
|
|
5538683d78 | ||
|
|
d62af34e98 | ||
|
|
209f9fda52 | ||
|
|
29187a90df | ||
|
|
512267159a | ||
| 68c8f47c8f | |||
|
|
17bf086ef2 | ||
|
|
dac5213354 | ||
|
|
3f203f326e | ||
|
|
7e9f1277d4 | ||
|
|
9d8adbbbc1 | ||
|
|
3430ef823e | ||
|
|
08a0fb1b60 | ||
|
|
dd438b597a | ||
|
|
8746d71af1 | ||
|
|
6a161c7133 | ||
| 79369c378a |
107 changed files with 13277 additions and 823 deletions
|
|
@ -63,3 +63,10 @@ GOOGLE_ALLOWED_DOMAIN=
|
||||||
# Note: if a Google-linked account also has TOTP enabled, sign-in still requires
|
# Note: if a Google-linked account also has TOTP enabled, sign-in still requires
|
||||||
# the authenticator code (Google is treated as the first factor). Accounts without
|
# the authenticator code (Google is treated as the first factor). Accounts without
|
||||||
# TOTP complete sign-in in one Google step.
|
# TOTP complete sign-in in one Google step.
|
||||||
|
|
||||||
|
# Playout / Master Control (MCR)
|
||||||
|
# Image tag the mam-api spawns when a channel starts. Build with:
|
||||||
|
# docker compose --profile build-only build playout
|
||||||
|
PLAYOUT_IMAGE=wild-dragon-playout:latest
|
||||||
|
# Base AMCP port — each channel binds to BASE + channel_id (in CasparCG terms).
|
||||||
|
PLAYOUT_AMCP_BASE_PORT=5250
|
||||||
|
|
|
||||||
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
|
||||||
|
|
|
||||||
101
WORK_LOG_PLAYOUT.md
Normal file
101
WORK_LOG_PLAYOUT.md
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
# Playout / Master Control — Implementation Work Log
|
||||||
|
|
||||||
|
**Branch:** `feat/playout-mcr` (off `main`)
|
||||||
|
**Started:** 2026-05-30
|
||||||
|
**Status:** Code complete, awaiting runtime validation
|
||||||
|
|
||||||
|
Tracks the build of the playout (MCR) subsystem against the design at
|
||||||
|
`docs/superpowers/specs/2026-05-30-playout-mcr-design.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Commit sequence
|
||||||
|
|
||||||
|
| # | Commit | Scope |
|
||||||
|
|---|--------|-------|
|
||||||
|
| 1 | `docs(playout)` | Design spec, §7 questions answered |
|
||||||
|
| 2 | `feat(mam-api): migration 029` | Six tables, failover columns, audio_normalized flag |
|
||||||
|
| 3 | `feat(worker): playout-stage` | S3 → /media + EBU R128 loudnorm + index.js wiring |
|
||||||
|
| 4 | `feat(playout): sidecar` | CasparCG image + AMCP shim, HLS preview consumer, fps-aware frame math |
|
||||||
|
| 5 | `feat(mam-api): /playout control plane + auto-failover` | Routes + scheduler health tick + restartChannel helper |
|
||||||
|
| 6 | `feat(web-ui): MCR page` | screens-playout, styles, app/shell/index.html wiring |
|
||||||
|
| 7 | `build(playout): compose wiring + .env knobs` | /media volume, queue addition, build-only service |
|
||||||
|
| 8 | `docs(playout): work log` | This file |
|
||||||
|
|
||||||
|
## Resolved §7 decisions (2026-05-30)
|
||||||
|
|
||||||
|
- **Audio loudness:** pre-normalize at stage time. ffmpeg `loudnorm` two-pass
|
||||||
|
(I=-23 LUFS, TP=-1 dBTP, LRA=11), linear mode preserves dynamics. Output
|
||||||
|
AAC 192k @ 48 kHz, video stream copied. Per-item `audio_normalized` flag
|
||||||
|
so re-stages of the same asset skip the pass.
|
||||||
|
- **Frame rate:** `1080p5994` default (was `1080i5994`). Per-channel
|
||||||
|
override allowed via `video_format`. `fpsFor(videoFormat)` helper in
|
||||||
|
the sidecar drives SEEK / LENGTH / transition-frames math.
|
||||||
|
- **Preview latency:** HLS v1. CasparCG runs a second FFMPEG consumer
|
||||||
|
alongside the primary output, writing `/media/live/<channel_id>/index.m3u8`
|
||||||
|
(~600 kbps, 2s segments, 6-window list). Web UI plays via the existing
|
||||||
|
HLS plumbing.
|
||||||
|
- **Failover:** auto-restart on healthy node for NDI/SRT/RTMP. Alert-only
|
||||||
|
for DeckLink (device-index pinning makes blind re-placement risky).
|
||||||
|
Scheduler tick (PG advisory lock, same lock as recorder schedules) polls
|
||||||
|
sidecar `/status`; ~3 missed checks → `restartChannel(id)` picks the most
|
||||||
|
recently-seen-online other node, bumps `restart_count`, calls `/start`.
|
||||||
|
|
||||||
|
## Architecture notes
|
||||||
|
|
||||||
|
**Sidecar model.** One CasparCG container per channel. Spawned by mam-api
|
||||||
|
via local Docker socket (primary node) or remote node-agent
|
||||||
|
`/sidecar/start`. Tracked in `playout_sidecars` plus `playout_channels.container_id`.
|
||||||
|
Killed on `/stop` or by `restartChannel` during failover.
|
||||||
|
|
||||||
|
**Media flow.**
|
||||||
|
```
|
||||||
|
S3 master/proxy → playout-stage worker → /media/playout/<assetId>.<ext>
|
||||||
|
(loudnormed, AAC@-23 LUFS)
|
||||||
|
↓
|
||||||
|
CasparCG channel #1
|
||||||
|
↓
|
||||||
|
primary consumer HLS consumer
|
||||||
|
(DeckLink/NDI/ ↓
|
||||||
|
SRT/RTMP) /media/live/<ch_id>/*.m3u8
|
||||||
|
```
|
||||||
|
|
||||||
|
**Port contention.** `assertDeckLinkFree()` blocks starting a SDI channel
|
||||||
|
when a recorder or another channel on the same node+device_index is active.
|
||||||
|
|
||||||
|
**Failover scope.** NDI/SRT/RTMP have no hardware tie, so any healthy
|
||||||
|
cluster_node is eligible. DeckLink channels surface an alert in the UI
|
||||||
|
(`status='error'` + `error_message`) and require operator intervention.
|
||||||
|
|
||||||
|
## Testing checklist
|
||||||
|
|
||||||
|
- [ ] Apply migration 029 on dev DB
|
||||||
|
- [ ] Build playout image: `docker compose --profile build-only build playout`
|
||||||
|
- [ ] Build web-ui (`screens-playout` joins the esbuild list automatically)
|
||||||
|
- [ ] Create channel via POST /api/v1/playout/channels (SRT first, no HW)
|
||||||
|
- [ ] Stage 2-3 assets to a playlist, verify loudnorm metadata in stderr
|
||||||
|
- [ ] Start channel → sidecar container appears in `docker ps`
|
||||||
|
- [ ] AMCP smoke: `telnet <host> 5250`, `VERSION`, `INFO`
|
||||||
|
- [ ] Play playlist; verify HLS at /media/live/<id>/index.m3u8
|
||||||
|
- [ ] Skip / pause / resume / stop
|
||||||
|
- [ ] As-run log: GET /api/v1/playout/channels/:id/asrun
|
||||||
|
- [ ] Kill sidecar container → scheduler should restart on another node
|
||||||
|
within ~3 ticks (~45s), restart_count increments
|
||||||
|
- [ ] DeckLink channel kill: status flips to 'error', NO restart attempt
|
||||||
|
- [ ] Try starting a decklink channel on a device_index already held by a
|
||||||
|
recorder → 409
|
||||||
|
- [ ] MCR UI smoke: nav entry visible, page renders, drag-drop adds items,
|
||||||
|
transport buttons hit the API
|
||||||
|
|
||||||
|
## Known gaps (deferred)
|
||||||
|
|
||||||
|
- No WebRTC preview (HLS-only v1 — 4-6s lag, fine for confidence monitor).
|
||||||
|
- No graphics/CG overlay layer in Phase A (templates land in Phase B).
|
||||||
|
- No Phase B scheduler / 24/7 wall-clock channel (schema is in place,
|
||||||
|
scheduler tick is not).
|
||||||
|
- No multi-channel grid view (one channel at a time per page).
|
||||||
|
- No timecode / remaining-duration overlay (would need CasparCG INFO poll).
|
||||||
|
- No audio level meters on the UI.
|
||||||
|
- `restartChannel` updates DB state and triggers `/start`; if the new node
|
||||||
|
also fails repeatedly, there's no exponential backoff yet — bounded only
|
||||||
|
by the manual stop button.
|
||||||
297
deploy/install-driver.sh
Normal file
297
deploy/install-driver.sh
Normal file
|
|
@ -0,0 +1,297 @@
|
||||||
|
#!/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"
|
||||||
|
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
|
||||||
|
# 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
|
||||||
|
}
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# 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,6 +33,9 @@ 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:-}"
|
||||||
AGENT_PORT="${AGENT_PORT:-7436}"
|
AGENT_PORT="${AGENT_PORT:-7436}"
|
||||||
|
|
@ -65,6 +69,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 +114,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"
|
||||||
|
|
|
||||||
|
|
@ -55,10 +55,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
|
||||||
|
|
||||||
|
|
@ -103,6 +118,7 @@ services:
|
||||||
# worker-l4: HEAVY tier (proxy/conform/trim) on the L4 (NVENC). Talks to
|
# worker-l4: HEAVY tier (proxy/conform/trim) on the L4 (NVENC). Talks to
|
||||||
# zampp1's Redis/Postgres/S3 over the LAN (.200). No promotion scanner here.
|
# zampp1's Redis/Postgres/S3 over the LAN (.200). No promotion scanner here.
|
||||||
worker-l4:
|
worker-l4:
|
||||||
|
profiles: [gpu]
|
||||||
build:
|
build:
|
||||||
context: ./services/worker
|
context: ./services/worker
|
||||||
dockerfile: Dockerfile.gpu
|
dockerfile: Dockerfile.gpu
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ services:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
- /mnt/NVME/MAM/wild-dragon-live:/live
|
- /mnt/NVME/MAM/wild-dragon-live:/live
|
||||||
- /mnt/NVME/MAM/wild-dragon-growing:/growing
|
- /mnt/NVME/MAM/wild-dragon-growing:/growing
|
||||||
|
- /mnt/NVME/MAM/wild-dragon-media:/media
|
||||||
- /mnt/NVME/MAM/sdk:/sdk
|
- /mnt/NVME/MAM/sdk:/sdk
|
||||||
- /dev/shm:/dev/shm
|
- /dev/shm:/dev/shm
|
||||||
- /run/dbus:/run/dbus
|
- /run/dbus:/run/dbus
|
||||||
|
|
@ -60,7 +61,14 @@ 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_AMCP_BASE_PORT: ${PLAYOUT_AMCP_BASE_PORT:-5250}
|
||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
reservations:
|
reservations:
|
||||||
|
|
@ -120,14 +128,16 @@ services:
|
||||||
# after the capability-routing split, so import jobs sat unprocessed and
|
# after the capability-routing split, so import jobs sat unprocessed and
|
||||||
# assets stayed `ingesting` forever. import is concurrency-1 + network-
|
# assets stayed `ingesting` forever. import is concurrency-1 + network-
|
||||||
# bound, so one consumer (this heavy/primary worker) is sufficient.
|
# bound, so one consumer (this heavy/primary worker) is sufficient.
|
||||||
WORKER_QUEUES: proxy,conform,trim,import
|
WORKER_QUEUES: proxy,conform,trim,import,playout-stage
|
||||||
RUN_PROMOTION: "true"
|
RUN_PROMOTION: "true"
|
||||||
PROXY_CONCURRENCY: "2"
|
PROXY_CONCURRENCY: "2"
|
||||||
|
PLAYOUT_MEDIA_DIR: /media
|
||||||
NVIDIA_VISIBLE_DEVICES: GPU-79afca3e-2ab2-a6ea-1c44-706c1f0a26d6
|
NVIDIA_VISIBLE_DEVICES: GPU-79afca3e-2ab2-a6ea-1c44-706c1f0a26d6
|
||||||
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
|
- /mnt/NVME/MAM/wild-dragon-growing:/growing
|
||||||
|
- /mnt/NVME/MAM/wild-dragon-media:/media
|
||||||
networks:
|
networks:
|
||||||
- wild-dragon
|
- wild-dragon
|
||||||
|
|
||||||
|
|
@ -176,12 +186,22 @@ services:
|
||||||
- "${PORT_WEB_UI:-7434}:80"
|
- "${PORT_WEB_UI:-7434}:80"
|
||||||
volumes:
|
volumes:
|
||||||
- /mnt/NVME/MAM/wild-dragon-live:/live
|
- /mnt/NVME/MAM/wild-dragon-live:/live
|
||||||
|
- /mnt/NVME/MAM/wild-dragon-media:/media:ro
|
||||||
- /dev/shm:/dev/shm
|
- /dev/shm:/dev/shm
|
||||||
- /run/dbus:/run/dbus
|
- /run/dbus:/run/dbus
|
||||||
- /run/systemd:/run/systemd
|
- /run/systemd:/run/systemd
|
||||||
networks:
|
networks:
|
||||||
- wild-dragon
|
- wild-dragon
|
||||||
|
|
||||||
|
# Build-only: the CasparCG sidecar image. mam-api spawns these on-demand per
|
||||||
|
# channel (one container per playout channel), so this service is never up'd —
|
||||||
|
# it exists so `docker compose build playout` produces the image the API tags
|
||||||
|
# via PLAYOUT_IMAGE. Profile excludes it from default `up`.
|
||||||
|
playout:
|
||||||
|
profiles: ["build-only"]
|
||||||
|
build: ./services/playout
|
||||||
|
image: wild-dragon-playout:latest
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
redis_data:
|
redis_data:
|
||||||
|
|
|
||||||
235
docs/superpowers/specs/2026-05-30-playout-mcr-design.md
Normal file
235
docs/superpowers/specs/2026-05-30-playout-mcr-design.md
Normal file
|
|
@ -0,0 +1,235 @@
|
||||||
|
# Wild Dragon MAM — Playout / Master Control (MCR)
|
||||||
|
|
||||||
|
**Date:** 2026-05-30 (revised 2026-05-30 — §7 closed)
|
||||||
|
**Status:** APPROVED — implementation in progress (code drafted but uncommitted; see WORK_LOG_PLAYOUT.md)
|
||||||
|
**Author:** Zac + Claude
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Resolved Decisions
|
||||||
|
|
||||||
|
| Question | Decision |
|
||||||
|
|----------|----------|
|
||||||
|
| Playout engine | **CasparCG Server** (orchestrated via AMCP), not ffmpeg-native |
|
||||||
|
| Channel count | **Multi-channel from start** — N independent channels, placed across cluster nodes by capability (mirrors recorders) |
|
||||||
|
| Scheduling model | **Phased** — Phase A: on-demand playlist player; Phase B: 24/7 continuous channel |
|
||||||
|
| Output targets | SDI (DeckLink), NDI, SRT, RTMP — all via CasparCG consumers |
|
||||||
|
| Media source | Assets live in **S3**; must be staged to a CasparCG-local media volume before play (see §4) |
|
||||||
|
| CasparCG packaging | **Build our own image** (like `capture/build-with-decklink.sh`) — GL context via GPU passthrough or Xvfb; NDI + DeckLink SDKs fetched at build time (not redistributable) |
|
||||||
|
| Master codec playability | Zac confirms current masters **play fine in CasparCG** — no transcode-for-playout step; staging is a plain S3→/media copy. Validate on hardware but do not gate on it |
|
||||||
|
| Management UI | **Single Dragonflight `playout.html` GUI** drives everything via AMCP; operator never touches CasparCG directly |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Playout adds **master-control-room** capability to Dragonflight: take library assets, arrange them on a timeline / playlist, and play them out continuously to a broadcast output — SDI via DeckLink, or stream via SRT / RTMP / NDI. Drag-and-drop scheduling, a program/preview monitor, and as-run logging.
|
||||||
|
|
||||||
|
This is the **mirror image** of the existing capture path. Capture is `input → ffmpeg encode → S3`. Playout is `S3 asset → engine → output`. We reuse three things wholesale:
|
||||||
|
|
||||||
|
1. **Cluster node + capability model** — nodes already advertise DeckLink/Deltacast/GPU in `cluster_nodes.capabilities`; channels are placed on nodes that have a free output port, exactly as recorders claim input ports.
|
||||||
|
2. **Sidecar orchestration** — mam-api spawns containers via the local Docker socket or the remote `node-agent /sidecar/start`. A CasparCG channel is just a different sidecar image.
|
||||||
|
3. **Scheduler tick + PG advisory lock** — `src/scheduler.js` already runs a single-leader tick over a schedule table. Phase B's wall-clock channel reuses this pattern.
|
||||||
|
|
||||||
|
### Why CasparCG over ffmpeg-native
|
||||||
|
|
||||||
|
The capture stack proves we can drive ffmpeg + DeckLink. But playout's hard part is **gapless, frame-accurate, clean transitions between clips** — every clip boundary in an ffmpeg-per-clip model is a black flash unless we engineer a concat-feeder. CasparCG solves this natively: a channel is a persistent output with a playlist, hard/mix/wipe transitions, layered graphics/logo (CG), and DeckLink/NDI/SRT/RTMP consumers built in. We orchestrate it over **AMCP** (its TCP control protocol) instead of reinventing a feeder. Trade: a new dependency + container image, and media must be on a CasparCG-visible disk (§4).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Data Model
|
||||||
|
|
||||||
|
New migration `029-playout.sql`. Five tables.
|
||||||
|
|
||||||
|
### 1.1 `playout_channels`
|
||||||
|
A logical output. One channel → one engine instance → one output target.
|
||||||
|
|
||||||
|
```
|
||||||
|
id uuid pk
|
||||||
|
name text -- "Channel 1", "Pop-up SDI"
|
||||||
|
node_id uuid -> cluster_nodes(id) -- where the engine runs (null = primary)
|
||||||
|
output_type text -- 'decklink' | 'ndi' | 'srt' | 'rtmp'
|
||||||
|
output_config jsonb -- { device_index } | { ndi_name } | { url, latency } | { url, key }
|
||||||
|
video_format text -- '1080i5994' | '1080p5994' | '720p5994' ...
|
||||||
|
status text -- 'stopped' | 'starting' | 'running' | 'error'
|
||||||
|
container_id text -- running CasparCG sidecar
|
||||||
|
project_id uuid -> projects(id) -- RBAC scoping (nullable = admin-only)
|
||||||
|
created_at / updated_at
|
||||||
|
```
|
||||||
|
|
||||||
|
`output_type` + `output_config` map straight to a CasparCG consumer:
|
||||||
|
- `decklink` → `ADD <ch> DECKLINK <device> ...`
|
||||||
|
- `ndi` → `ADD <ch> NDI ...`
|
||||||
|
- `srt`/`rtmp` → `ADD <ch> FFMPEG <url> -f mpegts ...` (CasparCG 2.3+ ffmpeg consumer)
|
||||||
|
|
||||||
|
### 1.2 `playout_playlists`
|
||||||
|
An ordered list of items bound to a channel. Phase A's primary object.
|
||||||
|
|
||||||
|
```
|
||||||
|
id, channel_id -> playout_channels(id)
|
||||||
|
name, loop boolean, created_at / updated_at
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3 `playout_items`
|
||||||
|
One entry on a playlist OR one entry on the 24/7 timeline.
|
||||||
|
|
||||||
|
```
|
||||||
|
id
|
||||||
|
playlist_id uuid -> playout_playlists(id) -- Phase A
|
||||||
|
asset_id uuid -> assets(id)
|
||||||
|
sort_order int -- position in playlist (Phase A)
|
||||||
|
scheduled_at timestamptz -- wall-clock start (Phase B, null in A)
|
||||||
|
in_point numeric -- seconds, trim head (reuse subclip in/out from editor)
|
||||||
|
out_point numeric -- seconds, trim tail
|
||||||
|
transition text -- 'cut' | 'mix' | 'wipe'
|
||||||
|
transition_ms int
|
||||||
|
graphics jsonb -- optional CG/template overlay (Phase B+)
|
||||||
|
media_status text -- 'pending' | 'staging' | 'ready' | 'error' (see §4)
|
||||||
|
media_path text -- resolved path inside the CasparCG media volume
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.4 `playout_schedule` (Phase B)
|
||||||
|
Day-ahead, wall-clock-bound timeline rows. Same shape as `playout_items` but `scheduled_at` is authoritative and the scheduler tick (§5) drives transitions. Phase A can ignore this table.
|
||||||
|
|
||||||
|
### 1.5 `playout_as_run`
|
||||||
|
Append-only log: what actually played, when, for how long. Compliance / billing.
|
||||||
|
|
||||||
|
```
|
||||||
|
id, channel_id, asset_id, item_id
|
||||||
|
started_at, ended_at, duration_s, result -- 'played' | 'skipped' | 'error'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Services & Components
|
||||||
|
|
||||||
|
### 2.1 New sidecar: `services/playout/` (CasparCG wrapper)
|
||||||
|
A thin container: **CasparCG Server** + a small Node control shim exposing HTTP, the same way `capture` wraps ffmpeg.
|
||||||
|
|
||||||
|
- Base image: official/community `casparcg/server` (Linux build with DeckLink + NDI + FFmpeg producers/consumers).
|
||||||
|
- Node shim (`src/index.js`): opens an AMCP TCP socket to local CasparCG, exposes:
|
||||||
|
- `POST /channel/start` → `ADD <ch> <consumer>` for the channel's output target
|
||||||
|
- `POST /play` → `PLAY <ch>-<layer> <media> [transition]`
|
||||||
|
- `POST /loadbg` + `/play` → preview/cue then take (preview monitor)
|
||||||
|
- `POST /stop`, `GET /status` → `INFO <ch>` (current clip, position, fps)
|
||||||
|
- playlist load → translate `playout_items` rows into a sequence of AMCP `LOADBG`/`PLAY` calls, advancing on `OnTransition` / end-of-clip events.
|
||||||
|
- Mirrors capture's status-polling contract so the UI monitor reuses existing plumbing.
|
||||||
|
|
||||||
|
### 2.2 mam-api: `src/routes/playout.js`
|
||||||
|
CRUD + control, RBAC-scoped via the existing `assertProjectAccess` helper (channels carry `project_id`).
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /playout/channels list (project-filtered)
|
||||||
|
POST /playout/channels create (edit on project)
|
||||||
|
POST /playout/channels/:id/start|stop spawn/kill CasparCG sidecar
|
||||||
|
GET /playout/channels/:id/status proxy engine INFO
|
||||||
|
POST /playout/channels/:id/play|pause|skip transport control
|
||||||
|
GET/POST/PUT/DELETE /playout/playlists... playlist + item CRUD, reorder
|
||||||
|
POST /playout/items/:id/stage kick S3→media-volume staging (§4)
|
||||||
|
GET /playout/channels/:id/asrun as-run log
|
||||||
|
```
|
||||||
|
|
||||||
|
Channel start/stop reuses `resolveNodeTarget()` + the Docker-socket / `node-agent /sidecar/start` split already in `recorders.js`. **Refactor opportunity:** lift that sidecar-spawn logic out of `recorders.js` into `src/orchestration/sidecar.js` so both recorders and playout share it (keep this small — only what both need).
|
||||||
|
|
||||||
|
### 2.3 web-ui: `playout.html` + `public/playout.jsx`
|
||||||
|
New MCR page. Layout:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─ PREVIEW ───────────┬─ PROGRAM (on air) ──────┐
|
||||||
|
│ [cued clip] │ [live output] ● ON AIR │
|
||||||
|
│ TC / duration │ TC / remaining │
|
||||||
|
│ [CUE] [TAKE] │ [PLAY][PAUSE][SKIP][STOP]│
|
||||||
|
├─ MEDIA BIN ─────────┴──────────────────────────┤
|
||||||
|
│ (draggable asset list, reuse asset browser) │
|
||||||
|
├─ PLAYLIST / TIMELINE ──────────────────────────┤
|
||||||
|
│ ▸ clip A ──▸ clip B ──▸ clip C (drag-drop) │ Phase A: ordered list
|
||||||
|
│ └ 24h time grid w/ now-bar │ Phase B: time-of-day grid
|
||||||
|
└────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
- Drag-drop: reuse whatever the NLE editor timeline uses (check `editor.jsx`); assets drag from the bin into the playlist/grid.
|
||||||
|
- API via existing `ZAMPP_API.fetch` wrapper.
|
||||||
|
- Program monitor: HLS preview of the output — CasparCG can emit a second low-bitrate FFmpeg consumer to HLS, reusing the `/live/<id>` HLS plumbing capture already uses.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Channel placement & ports
|
||||||
|
|
||||||
|
A DeckLink port is exclusive — same constraint capture already handles. A node's DeckLink port can be an **input (recorder)** or an **output (playout channel)**, never both at once. So:
|
||||||
|
|
||||||
|
- Extend the capability/port-claim check: when starting a channel on `output_type=decklink`, verify the target node has that device index free (no active recorder, no active channel).
|
||||||
|
- NDI / SRT / RTMP outputs have no hardware contention → can stack many per node (GPU/CPU-bound only).
|
||||||
|
- Surface a unified "device map" (extend the existing cluster DeckLink-status endpoint) showing each port's role: idle / recording / playing-out.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Media staging (the S3 ⇄ CasparCG gap)
|
||||||
|
|
||||||
|
**The crux.** Assets live in S3 (`original_s3_key` / `proxy_s3_key`). CasparCG plays from a **local media folder**. Options:
|
||||||
|
|
||||||
|
- **A — Pre-stage to a shared media volume (recommended).** Before a clip can go on air, download/symlink it from S3 to a CasparCG-visible volume (`/media`), set `playout_items.media_status='ready'` + `media_path`. A new BullMQ `playout-stage` job (reuses the worker pattern) does the pull. UI shows per-item readiness; TAKE is blocked until `ready`. Mirrors the growing-file SMB share already mounted for capture.
|
||||||
|
- **B — Stream from S3 via presigned URL.** CasparCG FFmpeg producer plays an HTTPS presigned URL directly. Zero staging, but seeking/trim and gapless transitions over network are fragile for broadcast. Acceptable as a fallback for SRT/RTMP, risky for SDI.
|
||||||
|
|
||||||
|
**Decision:** Phase A uses **A** (stage proxies for preview, masters for air) with **B** available as a per-channel "low-latency / no-stage" toggle. Zac confirms the current masters play fine in CasparCG, so staging is a **plain S3→/media copy — no transcode-for-playout step**. (Validate on hardware during implementation, but the model does not assume a transcode stage.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Scheduling
|
||||||
|
|
||||||
|
### Phase A — playlist player
|
||||||
|
No wall clock. Operator builds a `playout_playlists` row, drags items in, hits PLAY. The playout sidecar walks `playout_items` by `sort_order`, cueing the next clip during the current one (`LOADBG`) and taking it at end-of-clip with the configured transition. `loop` repeats. As-run logged per item.
|
||||||
|
|
||||||
|
### Phase B — 24/7 continuous channel
|
||||||
|
Wall-clock timeline in `playout_schedule`. Reuse `src/scheduler.js`:
|
||||||
|
- Add a second tick (or extend the existing one) under the **same PG advisory lock pattern** — exactly-one-leader, so a multi-replica deploy doesn't double-fire.
|
||||||
|
- Tick responsibilities: stage upcoming items (look-ahead window), verify the on-air item matches the schedule, **fill gaps** (loop a filler/slate asset when the timeline has a hole — a channel must never go black), roll the day forward.
|
||||||
|
- As-run becomes the compliance record.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Phasing / Milestones
|
||||||
|
|
||||||
|
**Phase A — Playlist playout MVP**
|
||||||
|
1. Migration `029-playout.sql` (channels, playlists, items, as-run).
|
||||||
|
2. `services/playout/` sidecar: CasparCG image + AMCP control shim, single output target (start with **SRT or NDI** — no hardware needed for dev; DeckLink behind hardware check).
|
||||||
|
3. mam-api `routes/playout.js` — channel + playlist CRUD, start/stop, transport, RBAC.
|
||||||
|
4. `playout-stage` BullMQ job (S3 → /media).
|
||||||
|
5. web-ui `playout.html` — bin + drag-drop ordered playlist + program/preview monitors + transport.
|
||||||
|
6. DeckLink output on real hardware; port-contention check vs recorders.
|
||||||
|
|
||||||
|
**Phase B — 24/7 continuous channel**
|
||||||
|
7. `playout_schedule` + time-of-day grid UI.
|
||||||
|
8. Scheduler tick (advisory-locked) — look-ahead staging, gap-fill/slate, day-roll.
|
||||||
|
9. As-run reporting view.
|
||||||
|
10. Graphics/CG overlay (logo bug, lower-thirds) via CasparCG templates.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Open Questions (for review)
|
||||||
|
|
||||||
|
**Resolved (2026-05-30):**
|
||||||
|
- ~~CasparCG packaging~~ → **build our own image.** Fetch DeckLink + NDI SDKs at build time (not redistributable — same as capture's DeckLink build). GL context for the mixer comes from GPU passthrough on a real node, or **Xvfb** (virtual framebuffer) where there's no display — community images run `--privileged` + X11 socket. Pin the NDI SDK version to what the server expects (`.so` version mismatch is the common docker failure).
|
||||||
|
- ~~Master codec playability~~ → Zac confirms masters **play fine**; no transcode-for-playout. Staging = plain S3→/media copy.
|
||||||
|
- ~~Management GUI~~ → **single Dragonflight `playout.html`** drives everything via AMCP; operator never touches CasparCG.
|
||||||
|
- ~~Audio loudness~~ → **pre-normalize at stage time** (Zac, 2026-05-30). `playout-stage` job runs ffmpeg `loudnorm` (EBU R128, target −23 LUFS, true-peak −1 dBTP) once, on the S3→/media copy. Output is the cached version CasparCG plays. Staging is no longer a pure copy — staging cost ≈ realtime CPU per clip on first stage; results are reusable across channels. Override (`media_status='ready'` + raw copy) available for clips already mastered to spec.
|
||||||
|
- ~~Frame rate~~ → **`1080p5994`** default for new channels (Zac, 2026-05-30). Progressive 1080 @ 59.94 fps. Per-channel override allowed (`video_format` column). Streaming-friendly (SRT/RTMP/NDI) and current SDI gear accepts it; matches capture's 59.94 cadence.
|
||||||
|
- ~~Preview latency~~ → **HLS v1** (Zac, 2026-05-30). Reuse capture's `/live/<id>` HLS plumbing. CasparCG emits a second low-bitrate FFmpeg consumer to HLS. ~4–6s lag, fine for confidence monitor. Operator desk gets a real downstream monitor off the SDI/NDI output anyway. Revisit WebRTC if MCR operators complain.
|
||||||
|
- ~~Failover~~ → **auto-restart on healthy node** (Zac, 2026-05-30). Scheduler tick (§5) monitors `playout_sidecars` health (AMCP ping + container alive); on N missed checks marks the channel `error`, re-places it on another capability-matching node with a free output port, resumes the playlist from the next item after the as-run-logged on-air item. Gap = black/slate for ~5–30 s during respawn (operator sees a flag in the UI). **DeckLink channels are not auto-failed-over in v1** — device-index pinning makes the destination port non-trivial; v1 alerts and lets the operator move the channel. NDI/SRT/RTMP channels (no hardware contention) failover automatically. Tracked via `restart_count` + `last_restart_at` on `playout_channels`.
|
||||||
|
|
||||||
|
**Still open:**
|
||||||
|
- (none — all §7 questions resolved 2026-05-30)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Reused building blocks (already in the repo)
|
||||||
|
|
||||||
|
| Need | Existing piece |
|
||||||
|
|------|----------------|
|
||||||
|
| Spawn engine container local/remote | `recorders.js` Docker-socket + `node-agent /sidecar/start` |
|
||||||
|
| Node capability / port model | `cluster_nodes.capabilities`, cluster DeckLink-status endpoint |
|
||||||
|
| Single-leader scheduled transitions | `src/scheduler.js` + PG advisory lock |
|
||||||
|
| Background media jobs | BullMQ worker (`services/worker`) |
|
||||||
|
| RBAC scoping | `src/auth/authz.js` `assertProjectAccess` (channel/project_id) |
|
||||||
|
| HLS preview plumbing | capture's `/live/<id>` HLS output |
|
||||||
|
| Subclip in/out points | NLE editor in/out marking |
|
||||||
|
| API wrapper / SPA shell | `ZAMPP_API.fetch`, esbuild JSX pages |
|
||||||
|
|
@ -0,0 +1,148 @@
|
||||||
|
# Storage Settings Warning + Growing-Files SMB Auth + Per-Recorder Growing Mode
|
||||||
|
**Date:** 2026-05-31
|
||||||
|
**Branch:** `feat/playout-mcr` (Forgejo: WildDragonLLC/dragonflight)
|
||||||
|
**Status:** Approved, ready for implementation plan
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
Three related refinements to the Settings page and growing-files capture pipeline:
|
||||||
|
|
||||||
|
1. **Storage warning header** — a prominent set-once warning at the top of the Storage settings section.
|
||||||
|
2. **Growing-files SMB credentials + system CIFS mount** — store an SMB username/password and have the capture stack mount the growing share itself (Approach A).
|
||||||
|
3. **Per-recorder growing mode** — remove the global "capture writes to local SMB share first" toggle; make growing-files mode a per-recorder setting.
|
||||||
|
|
||||||
|
All changes live on the `growing-files` / storage-settings path. No playout changes (handled separately).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Background (current state)
|
||||||
|
|
||||||
|
- **Settings storage:** single key/value `settings(key TEXT PRIMARY KEY, value TEXT, updated_at)` table (migration 006). Secrets like `s3_secret_key` are stored but `GET /settings/s3` returns only `s3_secret_key_exists` (never the value).
|
||||||
|
- **Growing settings UI:** `GrowingSettingsCard` in `services/web-ui/public/screens-admin.jsx` (rendered by `StorageSection` alongside `MountHealthStrip` and `S3SettingsCard`). Current keys: `growing_enabled`, `growing_path`, `growing_smb_url`, `growing_promote_after_seconds`.
|
||||||
|
- **Settings API:** `services/mam-api/src/routes/settings.js` — `GET/PUT /settings/growing` over `GROWING_KEYS`.
|
||||||
|
- **Global enable today:** the `growing_enabled` setting (checkbox labelled "Capture writes to the local SMB share first; Premier can edit while it's still growing"). `recorders.js` reads this global key at recorder-start and passes `GROWING_ENABLED=true/false` to the capture container.
|
||||||
|
- **Capture write path:** `services/capture/src/capture-manager.js` reads `process.env.GROWING_ENABLED` and `GROWING_PATH` (default `/growing`). When on, it writes the master to `/growing/{projectId}/{clipName}.{ext}` instead of streaming to S3; the promotion worker uploads to S3 after stop.
|
||||||
|
- **Current mount model:** `/growing` is a pre-mounted host bind-mount; the app never authenticates to SMB.
|
||||||
|
- **Per-recorder column already exists:** migration 014 added `recorders.growing_enabled BOOLEAN DEFAULT NULL` ("NULL = use global"), but recorder-start logic ignores it and the new-recorder modal does not expose it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 1 — Storage warning header
|
||||||
|
|
||||||
|
Add a danger-styled banner at the **top of `StorageSection()`**, above `MountHealthStrip`.
|
||||||
|
|
||||||
|
- Visual: full-width banner, danger token styling (`--danger` border + subtle danger background), alert icon, bold uppercase text.
|
||||||
|
- Exact copy:
|
||||||
|
> **⚠ WARNING — THESE SETTINGS ARE MEANT TO BE SET ONCE AT INITIAL DEPLOYMENT. CHANGING STORAGE PATHS MID-CYCLE CAN CAUSE DATABASE CORRUPTION AND FILE LOSS. PLEASE USE WITH CAUTION.**
|
||||||
|
- Pure presentational; no backend, no dismiss state (always visible).
|
||||||
|
|
||||||
|
**Files:** `services/web-ui/public/screens-admin.jsx` (and a small style rule in the appropriate CSS file if needed).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 2 — Growing-files SMB credentials + system CIFS mount (Approach A)
|
||||||
|
|
||||||
|
The growing share is **shared infrastructure**, so the SMB connection config is global.
|
||||||
|
|
||||||
|
### New settings keys
|
||||||
|
| Key | Purpose | Notes |
|
||||||
|
|-----|---------|-------|
|
||||||
|
| `growing_smb_mount` | CIFS source for the system mount, e.g. `//10.0.0.25/mam-growing` | Distinct from `growing_smb_url` |
|
||||||
|
| `growing_smb_username` | SMB user | Returned in GET (not secret) |
|
||||||
|
| `growing_smb_password` | SMB password | **Write-only** — never returned |
|
||||||
|
| `growing_smb_vers` *(optional)* | CIFS protocol version, default `3.0` | Avoids mount negotiation failures |
|
||||||
|
|
||||||
|
`growing_smb_url` (the `smb://…` string) is retained unchanged as the **editor-facing** display value (Premiere connect string).
|
||||||
|
|
||||||
|
### Settings API (`settings.js`)
|
||||||
|
- Extend `GROWING_KEYS` with the new keys (except the password is handled specially).
|
||||||
|
- `GET /settings/growing`: return `growing_smb_mount`, `growing_smb_username`, `growing_smb_vers`, and `growing_smb_password_exists: boolean` — **never** the password value. (Mirror the existing `s3_secret_key_exists` pattern.)
|
||||||
|
- `PUT /settings/growing`: upsert each provided key. For `growing_smb_password`, only write it when a non-empty value is provided (an empty/omitted field leaves the stored password unchanged). Provide a way to clear it (explicit empty sentinel or a "clear" affordance) — see Resolved decisions below.
|
||||||
|
|
||||||
|
### Settings UI (`GrowingSettingsCard`)
|
||||||
|
Add three fields to the card:
|
||||||
|
- **SMB mount (CIFS):** text input bound to `growing_smb_mount`, placeholder `//10.0.0.25/mam-growing`.
|
||||||
|
- **SMB username:** text input bound to `growing_smb_username`.
|
||||||
|
- **SMB password:** masked password input. Shows a "saved" indicator when `growing_smb_password_exists` is true; typing a new value replaces it; leaving it blank keeps the existing one. `autoComplete="new-password"`.
|
||||||
|
|
||||||
|
### Capture image (`services/capture/Dockerfile`)
|
||||||
|
Add `cifs-utils` to the installed packages so `mount -t cifs` is available inside the capture container.
|
||||||
|
|
||||||
|
### Capture-manager (`capture-manager.js`)
|
||||||
|
On capture start, when growing mode is active **and** `GROWING_SMB_MOUNT` is set:
|
||||||
|
1. Write a root-only credentials file (e.g. `/run/smb-creds`, mode `0600`) containing:
|
||||||
|
```
|
||||||
|
username=<GROWING_SMB_USERNAME>
|
||||||
|
password=<GROWING_SMB_PASSWORD>
|
||||||
|
```
|
||||||
|
(Credentials go in the file, **not** the mount command line, to avoid `ps`/log exposure.)
|
||||||
|
2. `mkdir -p $GROWING_PATH` then `mount -t cifs $GROWING_SMB_MOUNT $GROWING_PATH -o credentials=/run/smb-creds,uid=0,gid=0,file_mode=0664,dir_mode=0775,vers=$GROWING_SMB_VERS`.
|
||||||
|
3. If the mount succeeds → proceed writing the master to `$GROWING_PATH/...` (existing behaviour).
|
||||||
|
4. If the mount **fails** → log the error and fall back to S3 streaming (`growingPath = null`), so a recording is never lost.
|
||||||
|
5. On capture stop/cleanup, unmount `$GROWING_PATH` (best-effort; ignore "not mounted").
|
||||||
|
|
||||||
|
Mount isolation: each recorder runs in its **own** capture container, so each container mounts CIFS at its own private `/growing` — no cross-recorder mount conflicts, no ref-counting needed.
|
||||||
|
|
||||||
|
### Recorder start (`recorders.js`)
|
||||||
|
- Pass new env to the spawned capture container: `GROWING_SMB_MOUNT`, `GROWING_SMB_USERNAME`, `GROWING_SMB_PASSWORD`, `GROWING_SMB_VERS` (read from the `settings` table at start).
|
||||||
|
- The dynamically-spawned capture container must get `/growing` as an **empty mountpoint** (not a host bind-mount) so the in-container CIFS mount lands cleanly. Confirm/adjust the container spec accordingly. The container is already privileged (required for `mount`).
|
||||||
|
|
||||||
|
### Security notes
|
||||||
|
- The password is stored plaintext in the `settings` table, identical to the existing `s3_secret_key` handling — acceptable within this app's current secret model.
|
||||||
|
- The password reaches the capture container as an env var (visible via `docker inspect`), same as S3 keys already are. The credentials **file** (not the command line) is used for the actual mount.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 3 — Per-recorder growing mode (remove the global toggle)
|
||||||
|
|
||||||
|
### Remove global enable
|
||||||
|
- Delete the global "Capture writes to the local SMB share first…" checkbox (`growing_enabled` key) from `GrowingSettingsCard`. The card no longer carries a global on/off — it is **infrastructure-only**: container mount path, SMB URL (editor), SMB mount + credentials, promote threshold.
|
||||||
|
- The `growing_enabled` settings *key* is retired from the UI. (It may remain in the table harmlessly; recorder-start no longer reads it.)
|
||||||
|
|
||||||
|
### Per-recorder semantics
|
||||||
|
- Reuse the existing `recorders.growing_enabled BOOLEAN` column. New semantics (no global to defer to): `TRUE` = this recorder uses growing-files mode; `NULL`/`FALSE` = off.
|
||||||
|
- `recorders.js` recorder-start: read the **recorder's own** `growing_enabled` (defaulting `NULL`→off) and set `GROWING_ENABLED` for the capture container from that, instead of the global setting.
|
||||||
|
- Add `growing_enabled` to `RECORDER_FIELDS` so create/update accept it.
|
||||||
|
|
||||||
|
### UI
|
||||||
|
- **New-recorder modal** (`modal-new-recorder.jsx`): add a "Growing-files mode" toggle that sets `growing_enabled` on the created recorder (default off).
|
||||||
|
- **Recorder edit** (wherever recorders are edited): same toggle.
|
||||||
|
- Helper text on the toggle notes that growing-files requires the SMB share to be configured in Settings → Storage.
|
||||||
|
|
||||||
|
### Fallback
|
||||||
|
If a recorder has `growing_enabled = true` but `growing_smb_mount` is not configured globally, capture logs a warning and falls back to S3 streaming (same fallback path as a failed mount). Recording is never blocked.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files changed
|
||||||
|
|
||||||
|
| File | Change |
|
||||||
|
|------|--------|
|
||||||
|
| `services/web-ui/public/screens-admin.jsx` | Storage warning banner; SMB mount/username/password fields in `GrowingSettingsCard`; remove global growing-enable checkbox |
|
||||||
|
| `services/web-ui/public/modal-new-recorder.jsx` | Per-recorder "Growing-files mode" toggle |
|
||||||
|
| `services/mam-api/src/routes/settings.js` | New growing SMB keys; write-only password (`growing_smb_password_exists`) |
|
||||||
|
| `services/mam-api/src/routes/recorders.js` | Read per-recorder `growing_enabled`; pass SMB env to capture; `RECORDER_FIELDS` += `growing_enabled`; empty `/growing` mountpoint |
|
||||||
|
| `services/capture/Dockerfile` | Add `cifs-utils` |
|
||||||
|
| `services/capture/src/capture-manager.js` | CIFS mount-on-start (creds file), unmount-on-stop, fallback to S3 on failure |
|
||||||
|
| CSS (storage warning / fields) | Minor styles if needed |
|
||||||
|
|
||||||
|
No DB migration required (the `recorders.growing_enabled` column already exists; new settings are key/value rows).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Resolved decisions
|
||||||
|
|
||||||
|
- **Clearing the SMB password:** `PUT /settings/growing` treats a field value of the literal sentinel `""` with an explicit `growing_smb_password_clear: true` flag as "remove the stored password"; a blank field with no clear flag leaves it unchanged. (Keeps the common "don't retype the password on every save" UX while still allowing removal.)
|
||||||
|
- **CIFS version:** default `growing_smb_vers = 3.0`; overridable via settings to support older NAS targets.
|
||||||
|
- **Recorders already recording when the toggle changes:** the per-recorder `growing_enabled` is read at **start** only; changing it mid-recording has no effect on the active session (consistent with how all recorder encode settings already behave).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Out of scope (deferred)
|
||||||
|
|
||||||
|
- Encrypting secrets at rest (the app's existing model stores `s3_secret_key` in plaintext; SMB password follows the same model).
|
||||||
|
- A global "growing-files master kill switch" (removed by design — control is now per-recorder).
|
||||||
|
- Exposing `growing_retention_days` in the UI (seeded in DB, still unsurfaced; unrelated to this work).
|
||||||
|
- Playout HLS preview fix (handled by a separate parallel effort).
|
||||||
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.
|
||||||
|
|
@ -67,10 +67,14 @@ RUN /usr/local/bin/ffmpeg -hide_banner -encoders 2>&1 | grep -E 'nvenc' \
|
||||||
# ── Stage 2: Runtime image ───────────────────────────────────────────────────
|
# ── Stage 2: Runtime image ───────────────────────────────────────────────────
|
||||||
FROM node:20-bookworm
|
FROM node:20-bookworm
|
||||||
|
|
||||||
# Runtime deps for compiled ffmpeg libs
|
# Runtime deps for compiled ffmpeg libs.
|
||||||
|
# cifs-utils provides mount.cifs so growing-files capture can mount the SMB
|
||||||
|
# landing-zone share inside the (privileged) container at start (Approach A).
|
||||||
|
# util-linux supplies mount/umount/mountpoint.
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
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 \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Copy compiled ffmpeg/ffprobe
|
# Copy compiled ffmpeg/ffprobe
|
||||||
|
|
|
||||||
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
|
||||||
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { spawn } from 'child_process';
|
import { spawn, execFileSync } from 'child_process';
|
||||||
import { mkdirSync } from 'node:fs';
|
import { mkdirSync, writeFileSync, createReadStream, statSync, unlinkSync } from 'node:fs';
|
||||||
import { dirname } from 'node:path';
|
import { dirname } from 'node:path';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { createUploadStream } from './s3/client.js';
|
import { createUploadStream } from './s3/client.js';
|
||||||
|
|
@ -9,11 +9,88 @@ const S3_BUCKET = process.env.S3_BUCKET || 'wild-dragon';
|
||||||
// Growing-files mode: writes the master to a local SMB-backed share that the
|
// Growing-files mode: writes the master to a local SMB-backed share that the
|
||||||
// editor can mount, instead of streaming to S3 in real time. The promotion
|
// editor can mount, instead of streaming to S3 in real time. The promotion
|
||||||
// worker uploads the finalized file to S3 after the recording stops.
|
// worker uploads the finalized file to S3 after the recording stops.
|
||||||
// Toggled per-process by `GROWING_ENABLED=true` on the capture container
|
// Toggled per-recorder via `GROWING_ENABLED=true` on the capture container
|
||||||
// (see routes/recorders.js where the env is composed).
|
// (see routes/recorders.js where the env is composed).
|
||||||
const GROWING_ENABLED = process.env.GROWING_ENABLED === 'true';
|
const GROWING_ENABLED = process.env.GROWING_ENABLED === 'true';
|
||||||
const GROWING_PATH = process.env.GROWING_PATH || '/growing';
|
const GROWING_PATH = process.env.GROWING_PATH || '/growing';
|
||||||
|
|
||||||
|
// Approach A: when a CIFS source is supplied, this (privileged) container mounts
|
||||||
|
// the SMB landing-zone share at GROWING_PATH itself, using credentials supplied
|
||||||
|
// by the API from global Settings. Empty GROWING_SMB_MOUNT → no system mount
|
||||||
|
// (the host-bound /growing volume is used instead, or S3 streaming if growing
|
||||||
|
// is off).
|
||||||
|
// mount.cifs needs a UNC source (//host/share). Operators (and Settings) often
|
||||||
|
// store the share as an `smb://host/share` URL or a Windows `\\host\share`
|
||||||
|
// path; the kernel rejects those outright ("Mounting cifs URL not implemented
|
||||||
|
// yet"), which silently drops us back to S3. Normalize any of these forms to
|
||||||
|
// the `//host/share` UNC the mount helper accepts.
|
||||||
|
function toUncShare(raw) {
|
||||||
|
if (!raw) return '';
|
||||||
|
let s = String(raw).trim().replace(/\\/g, '/'); // \\host\share -> //host/share
|
||||||
|
s = s.replace(/^smb:\/\//i, '//'); // smb://host/share -> //host/share
|
||||||
|
if (!s.startsWith('//')) s = '//' + s.replace(/^\/+/, ''); // host/share -> //host/share
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
const GROWING_SMB_MOUNT = toUncShare(process.env.GROWING_SMB_MOUNT || '');
|
||||||
|
const GROWING_SMB_USERNAME = process.env.GROWING_SMB_USERNAME || '';
|
||||||
|
const GROWING_SMB_PASSWORD = process.env.GROWING_SMB_PASSWORD || '';
|
||||||
|
const GROWING_SMB_VERS = process.env.GROWING_SMB_VERS || '3.0';
|
||||||
|
const SMB_CREDS_FILE = '/run/smb-creds';
|
||||||
|
|
||||||
|
// True when GROWING_PATH is already a mountpoint (e.g. a prior session left it
|
||||||
|
// mounted, or a host bind-mount is present).
|
||||||
|
function isMounted(path) {
|
||||||
|
try { execFileSync('mountpoint', ['-q', path]); return true; }
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mount the CIFS growing share at GROWING_PATH. Credentials go in a root-only
|
||||||
|
// file (NOT the command line) so they never appear in `ps`/process listings.
|
||||||
|
// Returns true on success (or if already mounted), false on failure — callers
|
||||||
|
// fall back to S3 streaming so a recording is never lost.
|
||||||
|
function mountGrowingShare() {
|
||||||
|
if (!GROWING_SMB_MOUNT) return false;
|
||||||
|
try {
|
||||||
|
if (isMounted(GROWING_PATH)) {
|
||||||
|
console.log('[capture] growing share already mounted at', GROWING_PATH);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
try { mkdirSync(GROWING_PATH, { recursive: true }); } catch (_) {}
|
||||||
|
writeFileSync(
|
||||||
|
SMB_CREDS_FILE,
|
||||||
|
`username=${GROWING_SMB_USERNAME}\npassword=${GROWING_SMB_PASSWORD}\n`,
|
||||||
|
{ mode: 0o600 }
|
||||||
|
);
|
||||||
|
const opts = [
|
||||||
|
`credentials=${SMB_CREDS_FILE}`,
|
||||||
|
'uid=0', 'gid=0', 'file_mode=0664', 'dir_mode=0775',
|
||||||
|
`vers=${GROWING_SMB_VERS}`,
|
||||||
|
].join(',');
|
||||||
|
execFileSync('mount', ['-t', 'cifs', GROWING_SMB_MOUNT, GROWING_PATH, '-o', opts],
|
||||||
|
{ stdio: ['ignore', 'ignore', 'pipe'] });
|
||||||
|
console.log('[capture] mounted CIFS growing share', GROWING_SMB_MOUNT, '->', GROWING_PATH);
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
const stderr = err.stderr ? err.stderr.toString().trim() : err.message;
|
||||||
|
console.error('[capture] CIFS mount failed (falling back to S3 streaming):', stderr);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Best-effort unmount on session stop. Ignores "not mounted".
|
||||||
|
function unmountGrowingShare() {
|
||||||
|
if (!GROWING_SMB_MOUNT) return;
|
||||||
|
try {
|
||||||
|
if (isMounted(GROWING_PATH)) {
|
||||||
|
execFileSync('umount', [GROWING_PATH], { stdio: ['ignore', 'ignore', 'pipe'] });
|
||||||
|
console.log('[capture] unmounted growing share at', GROWING_PATH);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
const stderr = err.stderr ? err.stderr.toString().trim() : err.message;
|
||||||
|
console.warn('[capture] growing share unmount failed (ignored):', stderr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Codec catalogue ──────────────────────────────────────────────────────
|
// ── Codec catalogue ──────────────────────────────────────────────────────
|
||||||
// Each entry maps the UI value to a base ffmpeg flag set. Bitrate / framerate
|
// Each entry maps the UI value to a base ffmpeg flag set. Bitrate / framerate
|
||||||
// / pix_fmt are layered on top from the per-recorder configuration.
|
// / pix_fmt are layered on top from the per-recorder configuration.
|
||||||
|
|
@ -82,6 +159,7 @@ function buildEncodeArgs({
|
||||||
codec, videoBitrate, framerate,
|
codec, videoBitrate, framerate,
|
||||||
audioCodec, audioBitrate, audioChannels,
|
audioCodec, audioBitrate, audioChannels,
|
||||||
container, isNetwork, isProxy = false,
|
container, isNetwork, isProxy = false,
|
||||||
|
growing = false,
|
||||||
}) {
|
}) {
|
||||||
const v = VIDEO_CODECS[codec] || (isProxy ? VIDEO_CODECS.h264 : VIDEO_CODECS.prores_hq);
|
const v = VIDEO_CODECS[codec] || (isProxy ? VIDEO_CODECS.h264 : VIDEO_CODECS.prores_hq);
|
||||||
const a = AUDIO_CODECS[audioCodec] || (isProxy ? AUDIO_CODECS.aac : AUDIO_CODECS.pcm_s24le);
|
const a = AUDIO_CODECS[audioCodec] || (isProxy ? AUDIO_CODECS.aac : AUDIO_CODECS.pcm_s24le);
|
||||||
|
|
@ -99,9 +177,24 @@ function buildEncodeArgs({
|
||||||
if (a.bitrateControl && audioBitrate) args.push('-b:a', audioBitrate);
|
if (a.bitrateControl && audioBitrate) args.push('-b:a', audioBitrate);
|
||||||
if (audioChannels) args.push('-ac', String(audioChannels));
|
if (audioChannels) args.push('-ac', String(audioChannels));
|
||||||
|
|
||||||
|
// moov-atom placement is the difference between a Premiere-openable master and
|
||||||
|
// a "file cannot be opened" error.
|
||||||
|
//
|
||||||
|
// - Growing-file masters (edit-while-record on the SMB share) MUST be
|
||||||
|
// fragmented so a moov/mvex is present from the first frame and the file is
|
||||||
|
// decodable while still being written. The samples live in moof/trun boxes.
|
||||||
|
//
|
||||||
|
// - Finalized masters (the S3-piped recording that stops cleanly) must NOT be
|
||||||
|
// fragmented. Adobe Premiere's QuickTime/MOV importer reads the classic
|
||||||
|
// stco/stsz/stts sample tables in a single top-level moov; a fragmented MOV
|
||||||
|
// (moof/trun, empty sample tables) makes Premiere report "file cannot be
|
||||||
|
// opened." We write a clean, non-fragmented MOV instead.
|
||||||
|
// `+faststart` puts the moov before mdat on the second pass so the file is
|
||||||
|
// instantly seekable/streamable too.
|
||||||
if (fmt === 'mov' || fmt === 'mp4') {
|
if (fmt === 'mov' || fmt === 'mp4') {
|
||||||
args.push('-movflags', '+frag_keyframe+empty_moov');
|
args.push('-movflags', growing ? '+frag_keyframe+empty_moov+default_base_moof' : '+faststart');
|
||||||
}
|
}
|
||||||
|
// ProRes-in-MOV must carry a QuickTime brand or some importers reject the tag.
|
||||||
args.push('-f', fmt);
|
args.push('-f', fmt);
|
||||||
|
|
||||||
return args;
|
return args;
|
||||||
|
|
@ -283,7 +376,15 @@ class CaptureManager {
|
||||||
|
|
||||||
// Growing-files: write master to the local SMB share instead of streaming
|
// Growing-files: write master to the local SMB share instead of streaming
|
||||||
// to S3. Path is relative to the container's GROWING_PATH mount.
|
// to S3. Path is relative to the container's GROWING_PATH mount.
|
||||||
const growingPath = GROWING_ENABLED
|
//
|
||||||
|
// Approach A: if a CIFS source is configured, mount it now. A mount failure
|
||||||
|
// is non-fatal — we fall back to S3 streaming so the recording is never
|
||||||
|
// lost.
|
||||||
|
let growingActive = GROWING_ENABLED;
|
||||||
|
if (growingActive && GROWING_SMB_MOUNT) {
|
||||||
|
if (!mountGrowingShare()) growingActive = false; // fall back to S3
|
||||||
|
}
|
||||||
|
const growingPath = growingActive
|
||||||
? `${GROWING_PATH}/${projectId}/${clipName}.${hiresExt}`
|
? `${GROWING_PATH}/${projectId}/${clipName}.${hiresExt}`
|
||||||
: null;
|
: null;
|
||||||
if (growingPath) {
|
if (growingPath) {
|
||||||
|
|
@ -314,17 +415,39 @@ class CaptureManager {
|
||||||
container,
|
container,
|
||||||
isNetwork,
|
isNetwork,
|
||||||
isProxy: false,
|
isProxy: false,
|
||||||
|
// Only the growing-file master (written to the SMB share for
|
||||||
|
// edit-while-record) needs a fragmented MOV. The finalized, S3-piped
|
||||||
|
// master must be a clean non-fragmented MOV so Premiere can open it.
|
||||||
|
growing: !!growingPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('[capture] hires ffmpeg args:', hiresCodecArgs.join(' '));
|
console.log('[capture] hires ffmpeg args:', hiresCodecArgs.join(' '));
|
||||||
|
|
||||||
const sdiFilterArgs = (sourceType === 'sdi') ? ['-vf', 'yadif=mode=1:deint=1'] : [];
|
const sdiFilterArgs = (sourceType === 'sdi') ? ['-vf', 'yadif=mode=1:deint=1'] : [];
|
||||||
|
|
||||||
// When growing-files is on, write directly to the SMB share so Premier
|
// Master output destination.
|
||||||
// can mount and edit the live file. Promotion worker uploads to S3 on EOF.
|
//
|
||||||
// Otherwise, stream the master to S3 via stdout pipe (legacy behavior).
|
// - Growing-files on → write directly to the SMB share (fragmented MOV) so
|
||||||
const hiresOutput = growingPath ? growingPath : 'pipe:1';
|
// Premiere can mount and edit the live file; promotion worker uploads on EOF.
|
||||||
const hiresStdio = growingPath ? ['ignore', 'ignore', 'pipe'] : ['ignore', 'pipe', 'pipe'];
|
//
|
||||||
|
// - Growing-files off → write to a LOCAL SEEKABLE temp file, then upload to
|
||||||
|
// S3 on stop. We must NOT pipe the MOV muxer to S3 directly: the MOV/MP4
|
||||||
|
// muxer cannot write to a non-seekable pipe without `empty_moov`, and an
|
||||||
|
// empty_moov/fragmented MOV is exactly what makes Adobe Premiere report
|
||||||
|
// "file cannot be opened" (no classic stco/stsz sample tables — samples
|
||||||
|
// live in moof/trun). A seekable file lets ffmpeg write a single
|
||||||
|
// contiguous moov with full sample tables and `+faststart` moves it to the
|
||||||
|
// front, producing a master Premiere opens natively.
|
||||||
|
const localMasterPath = growingPath
|
||||||
|
? null
|
||||||
|
: `/tmp/capture/${sessionId}.${hiresExt}`;
|
||||||
|
if (localMasterPath) {
|
||||||
|
try { mkdirSync(dirname(localMasterPath), { recursive: true }); }
|
||||||
|
catch (err) { console.error('[capture] could not create temp master dir:', err.message); }
|
||||||
|
}
|
||||||
|
const hiresOutput = growingPath ? growingPath : localMasterPath;
|
||||||
|
// ffmpeg now writes a file (not stdout) in both modes → stdout is unused.
|
||||||
|
const hiresStdio = ['ignore', 'ignore', 'pipe'];
|
||||||
|
|
||||||
// For SDI we cannot open the DeckLink device a second time for a preview
|
// For SDI we cannot open the DeckLink device a second time for a preview
|
||||||
// tee, so the live HLS preview is produced as a SECOND OUTPUT of the hires
|
// tee, so the live HLS preview is produced as a SECOND OUTPUT of the hires
|
||||||
|
|
@ -359,12 +482,12 @@ class CaptureManager {
|
||||||
|
|
||||||
const hiresProcess = spawn('ffmpeg', hiresArgs, { stdio: hiresStdio });
|
const hiresProcess = spawn('ffmpeg', hiresArgs, { stdio: hiresStdio });
|
||||||
|
|
||||||
const hiresUpload = growingPath
|
// Growing-files: nothing to upload here (promotion worker handles S3).
|
||||||
? Promise.resolve({ growingPath })
|
// Non-growing: the master is uploaded from the finalized local file in
|
||||||
: createUploadStream(S3_BUCKET, hiresKey, hiresProcess.stdout);
|
// stop(), once ffmpeg has written the moov and exited cleanly — we can't
|
||||||
|
// upload while recording because the file isn't a valid MOV until finalize.
|
||||||
const processes = { hires: hiresProcess };
|
const processes = { hires: hiresProcess };
|
||||||
const uploads = { hires: hiresUpload };
|
const uploads = { hires: growingPath ? Promise.resolve({ growingPath }) : null };
|
||||||
|
|
||||||
// ── HLS tee for network sources (live preview in the UI) ──────────
|
// ── HLS tee for network sources (live preview in the UI) ──────────
|
||||||
let hlsProcess = null;
|
let hlsProcess = null;
|
||||||
|
|
@ -430,6 +553,7 @@ class CaptureManager {
|
||||||
hiresKey,
|
hiresKey,
|
||||||
proxyKey,
|
proxyKey,
|
||||||
growingPath,
|
growingPath,
|
||||||
|
localMasterPath,
|
||||||
startedAt,
|
startedAt,
|
||||||
duration: 0,
|
duration: 0,
|
||||||
uploads,
|
uploads,
|
||||||
|
|
@ -451,12 +575,56 @@ class CaptureManager {
|
||||||
|
|
||||||
const { processes, currentSession } = this.state;
|
const { processes, currentSession } = this.state;
|
||||||
|
|
||||||
|
// Send SIGINT and WAIT for ffmpeg to exit. This is what flushes the MOV
|
||||||
|
// trailer (writes the moov atom with the full sample tables). If we uploaded
|
||||||
|
// before ffmpeg finalized, the object would have no moov → "moov atom not
|
||||||
|
// found" / "file cannot be opened" in Premiere.
|
||||||
|
const waitExit = (proc) => new Promise((resolve) => {
|
||||||
|
if (!proc || proc.exitCode !== null || proc.signalCode !== null) return resolve();
|
||||||
|
let done = false;
|
||||||
|
const finish = () => { if (!done) { done = true; resolve(); } };
|
||||||
|
proc.once('exit', finish);
|
||||||
|
// Safety net: don't hang stop() forever if ffmpeg refuses to exit.
|
||||||
|
setTimeout(() => { try { proc.kill('SIGKILL'); } catch (_) {} finish(); }, 15000);
|
||||||
|
});
|
||||||
|
|
||||||
if (processes.hires) processes.hires.kill('SIGINT');
|
if (processes.hires) processes.hires.kill('SIGINT');
|
||||||
if (processes.proxy) processes.proxy.kill('SIGINT');
|
if (processes.proxy) processes.proxy.kill('SIGINT');
|
||||||
if (processes.hls) { try { processes.hls.kill('SIGINT'); } catch (_) {} }
|
if (processes.hls) { try { processes.hls.kill('SIGINT'); } catch (_) {} }
|
||||||
|
|
||||||
|
// Wait for the master writer to finalize before we read/upload the file.
|
||||||
|
await waitExit(processes.hires);
|
||||||
|
|
||||||
|
// Release the CIFS mount (best-effort) once the ffmpeg writers are done with
|
||||||
|
// it. The promotion worker reads the staged file from the host/S3 side, not
|
||||||
|
// through this container's mount, so unmounting here is safe.
|
||||||
|
unmountGrowingShare();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const uploadPromises = [currentSession.uploads.hires];
|
const uploadPromises = [];
|
||||||
|
|
||||||
|
// Non-growing: upload the finalized local master file to S3 now that the
|
||||||
|
// moov has been written. Growing: the promotion worker handles S3.
|
||||||
|
if (currentSession.localMasterPath) {
|
||||||
|
let size = 0;
|
||||||
|
try { size = statSync(currentSession.localMasterPath).size; } catch (_) {}
|
||||||
|
if (size > 0) {
|
||||||
|
uploadPromises.push(
|
||||||
|
createUploadStream(
|
||||||
|
S3_BUCKET,
|
||||||
|
currentSession.hiresKey,
|
||||||
|
createReadStream(currentSession.localMasterPath),
|
||||||
|
).then(() => {
|
||||||
|
try { unlinkSync(currentSession.localMasterPath); } catch (_) {}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.warn('[capture] local master is 0 bytes — skipping upload:', currentSession.localMasterPath);
|
||||||
|
}
|
||||||
|
} else if (currentSession.uploads.hires) {
|
||||||
|
uploadPromises.push(currentSession.uploads.hires);
|
||||||
|
}
|
||||||
|
|
||||||
if (currentSession.uploads.proxy) uploadPromises.push(currentSession.uploads.proxy);
|
if (currentSession.uploads.proxy) uploadPromises.push(currentSession.uploads.proxy);
|
||||||
await Promise.all(uploadPromises);
|
await Promise.all(uploadPromises);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
165
services/mam-api/src/db/migrations/029-playout.sql
Normal file
165
services/mam-api/src/db/migrations/029-playout.sql
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
-- Migration 029 — Playout / Master Control (MCR).
|
||||||
|
--
|
||||||
|
-- Adds a broadcast playout subsystem: take library assets, arrange them on a
|
||||||
|
-- playlist (Phase A) or a wall-clock timeline (Phase B), and play them out
|
||||||
|
-- continuously to SDI (DeckLink) / NDI / SRT / RTMP via a CasparCG sidecar.
|
||||||
|
--
|
||||||
|
-- This is the mirror of the capture path (input -> ffmpeg -> S3). A channel is
|
||||||
|
-- placed on a cluster node by capability the same way recorders claim input
|
||||||
|
-- ports; the engine container is spawned via the same Docker-socket /
|
||||||
|
-- node-agent orchestration. See docs/superpowers/specs/2026-05-30-playout-mcr-design.md.
|
||||||
|
--
|
||||||
|
-- Tables:
|
||||||
|
-- playout_channels — a logical output (one channel -> one CasparCG instance -> one target)
|
||||||
|
-- playout_playlists — an ordered list of items bound to a channel (Phase A)
|
||||||
|
-- playout_items — one clip on a playlist OR one row on the timeline
|
||||||
|
-- playout_sidecars — running CasparCG sidecar registry (one per channel; health-checked)
|
||||||
|
-- playout_schedule — wall-clock day-ahead rows (Phase B; unused in A)
|
||||||
|
-- playout_as_run — append-only log of what actually played (compliance)
|
||||||
|
|
||||||
|
-- ── Channels ───────────────────────────────────────────────────────────────
|
||||||
|
CREATE TABLE IF NOT EXISTS playout_channels (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
node_id UUID REFERENCES cluster_nodes(id) ON DELETE SET NULL,
|
||||||
|
output_type TEXT NOT NULL DEFAULT 'srt',
|
||||||
|
-- output_config is consumer-shape-specific:
|
||||||
|
-- decklink: { "device_index": 1 }
|
||||||
|
-- ndi: { "ndi_name": "DRAGONFLIGHT CH1" }
|
||||||
|
-- srt: { "url": "srt://host:9000", "latency": 200 }
|
||||||
|
-- rtmp: { "url": "rtmp://host/live", "key": "streamkey" }
|
||||||
|
output_config JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||||
|
-- 1080p59.94 is the house standard (matches capture cadence, streaming-friendly,
|
||||||
|
-- accepted by current SDI gear). Per-channel override allowed.
|
||||||
|
video_format TEXT NOT NULL DEFAULT '1080p5994',
|
||||||
|
status TEXT NOT NULL DEFAULT 'stopped',
|
||||||
|
container_id TEXT,
|
||||||
|
-- For remote channels the node-agent reports the reachable host:port of the
|
||||||
|
-- sidecar HTTP shim; stored here so the API can proxy transport calls.
|
||||||
|
container_meta JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||||
|
error_message TEXT,
|
||||||
|
-- Failover bookkeeping. Scheduler tick health-checks the sidecar; on N missed
|
||||||
|
-- checks the channel is re-placed on a healthy node (auto for ndi/srt/rtmp,
|
||||||
|
-- alert-only for decklink — device-index pinning makes re-placement non-trivial).
|
||||||
|
restart_count INTEGER NOT NULL DEFAULT 0,
|
||||||
|
last_restart_at TIMESTAMPTZ,
|
||||||
|
last_heartbeat_at TIMESTAMPTZ,
|
||||||
|
-- RBAC scoping: a NULL project_id resolves to admin-only (authz.js), the same
|
||||||
|
-- convention recorders use for unassigned resources.
|
||||||
|
project_id UUID REFERENCES projects(id) ON DELETE SET NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
CHECK (output_type IN ('decklink','ndi','srt','rtmp')),
|
||||||
|
CHECK (status IN ('stopped','starting','running','error'))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_playout_channels_node ON playout_channels (node_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_playout_channels_project ON playout_channels (project_id);
|
||||||
|
|
||||||
|
-- ── Playlists ──────────────────────────────────────────────────────────────
|
||||||
|
CREATE TABLE IF NOT EXISTS playout_playlists (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
channel_id UUID NOT NULL REFERENCES playout_channels(id) ON DELETE CASCADE,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
loop BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_playout_playlists_channel ON playout_playlists (channel_id);
|
||||||
|
|
||||||
|
-- ── Items ──────────────────────────────────────────────────────────────────
|
||||||
|
-- One entry on a playlist (Phase A, ordered by sort_order) OR one entry on the
|
||||||
|
-- timeline (Phase B, ordered by scheduled_at). in/out points reuse the editor's
|
||||||
|
-- subclip trim model (seconds). media_status tracks the S3 -> /media staging
|
||||||
|
-- (see playout-stage worker job); a clip cannot go on air until 'ready'.
|
||||||
|
CREATE TABLE IF NOT EXISTS playout_items (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
playlist_id UUID REFERENCES playout_playlists(id) ON DELETE CASCADE,
|
||||||
|
asset_id UUID NOT NULL REFERENCES assets(id) ON DELETE CASCADE,
|
||||||
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||||
|
scheduled_at TIMESTAMPTZ,
|
||||||
|
in_point NUMERIC,
|
||||||
|
out_point NUMERIC,
|
||||||
|
transition TEXT NOT NULL DEFAULT 'cut',
|
||||||
|
transition_ms INTEGER NOT NULL DEFAULT 0,
|
||||||
|
graphics JSONB,
|
||||||
|
media_status TEXT NOT NULL DEFAULT 'pending',
|
||||||
|
media_path TEXT,
|
||||||
|
-- Set when playout-stage has run loudnorm (EBU R128, -23 LUFS / -1 dBTP) on
|
||||||
|
-- the staged file. Re-stages skip the loudnorm pass when true.
|
||||||
|
audio_normalized BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
CHECK (transition IN ('cut','mix','wipe')),
|
||||||
|
CHECK (media_status IN ('pending','staging','ready','error'))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_playout_items_playlist ON playout_items (playlist_id, sort_order);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_playout_items_asset ON playout_items (asset_id);
|
||||||
|
|
||||||
|
-- ── Sidecars ───────────────────────────────────────────────────────────────
|
||||||
|
-- Running CasparCG container registry, one row per running channel. The
|
||||||
|
-- scheduler tick (src/scheduler.js) pings each sidecar's /status endpoint and
|
||||||
|
-- updates last_heartbeat_at; missed checks trigger the failover path in
|
||||||
|
-- routes/playout.js.
|
||||||
|
CREATE TABLE IF NOT EXISTS playout_sidecars (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
channel_id UUID NOT NULL REFERENCES playout_channels(id) ON DELETE CASCADE,
|
||||||
|
node_id UUID REFERENCES cluster_nodes(id) ON DELETE SET NULL,
|
||||||
|
container_id TEXT NOT NULL,
|
||||||
|
sidecar_url TEXT, -- http://host:port for the shim
|
||||||
|
amcp_port INTEGER, -- in-container AMCP port (default 5250)
|
||||||
|
status TEXT NOT NULL DEFAULT 'running',
|
||||||
|
last_heartbeat_at TIMESTAMPTZ,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
CHECK (status IN ('starting','running','error','stopped'))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_playout_sidecars_channel ON playout_sidecars (channel_id)
|
||||||
|
WHERE status IN ('starting','running');
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_playout_sidecars_status ON playout_sidecars (status);
|
||||||
|
|
||||||
|
-- ── Schedule (Phase B) ─────────────────────────────────────────────────────
|
||||||
|
-- Wall-clock day-ahead timeline. The scheduler tick (src/scheduler.js, under
|
||||||
|
-- the existing PG advisory lock) drives transitions and gap-fill. Unused by the
|
||||||
|
-- Phase A playlist player but created now so the schema is stable.
|
||||||
|
CREATE TABLE IF NOT EXISTS playout_schedule (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
channel_id UUID NOT NULL REFERENCES playout_channels(id) ON DELETE CASCADE,
|
||||||
|
asset_id UUID REFERENCES assets(id) ON DELETE SET NULL,
|
||||||
|
scheduled_at TIMESTAMPTZ NOT NULL,
|
||||||
|
in_point NUMERIC,
|
||||||
|
out_point NUMERIC,
|
||||||
|
transition TEXT NOT NULL DEFAULT 'cut',
|
||||||
|
transition_ms INTEGER NOT NULL DEFAULT 0,
|
||||||
|
is_filler BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
status TEXT NOT NULL DEFAULT 'scheduled',
|
||||||
|
media_status TEXT NOT NULL DEFAULT 'pending',
|
||||||
|
media_path TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
CHECK (transition IN ('cut','mix','wipe')),
|
||||||
|
CHECK (status IN ('scheduled','playing','played','skipped','error')),
|
||||||
|
CHECK (media_status IN ('pending','staging','ready','error'))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_playout_schedule_channel_time ON playout_schedule (channel_id, scheduled_at);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_playout_schedule_status ON playout_schedule (status, scheduled_at);
|
||||||
|
|
||||||
|
-- ── As-run log ─────────────────────────────────────────────────────────────
|
||||||
|
-- Append-only record of what actually went to air. Never updated after insert.
|
||||||
|
CREATE TABLE IF NOT EXISTS playout_as_run (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
channel_id UUID NOT NULL REFERENCES playout_channels(id) ON DELETE CASCADE,
|
||||||
|
asset_id UUID REFERENCES assets(id) ON DELETE SET NULL,
|
||||||
|
item_id UUID,
|
||||||
|
clip_name TEXT,
|
||||||
|
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
ended_at TIMESTAMPTZ,
|
||||||
|
duration_s NUMERIC,
|
||||||
|
result TEXT NOT NULL DEFAULT 'played',
|
||||||
|
CHECK (result IN ('played','skipped','error'))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_playout_as_run_channel ON playout_as_run (channel_id, started_at DESC);
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -22,6 +22,7 @@ import jobsRouter from './routes/jobs.js';
|
||||||
import captureRouter from './routes/capture.js';
|
import captureRouter from './routes/capture.js';
|
||||||
import uploadRouter from './routes/upload.js';
|
import uploadRouter from './routes/upload.js';
|
||||||
import recordersRouter from './routes/recorders.js';
|
import recordersRouter from './routes/recorders.js';
|
||||||
|
import playoutRouter from './routes/playout.js';
|
||||||
import settingsRouter from './routes/settings.js';
|
import settingsRouter from './routes/settings.js';
|
||||||
import amppRouter from './routes/ampp.js';
|
import amppRouter from './routes/ampp.js';
|
||||||
import groupsRouter from './routes/groups.js';
|
import groupsRouter from './routes/groups.js';
|
||||||
|
|
@ -40,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);
|
||||||
},
|
},
|
||||||
|
|
@ -59,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');
|
||||||
|
|
@ -74,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: {
|
||||||
|
|
@ -94,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);
|
||||||
|
|
@ -132,6 +107,7 @@ app.use('/api/v1/jobs', jobsRouter);
|
||||||
app.use('/api/v1/capture', captureRouter);
|
app.use('/api/v1/capture', captureRouter);
|
||||||
app.use('/api/v1/upload', uploadRouter);
|
app.use('/api/v1/upload', uploadRouter);
|
||||||
app.use('/api/v1/recorders', recordersRouter);
|
app.use('/api/v1/recorders', recordersRouter);
|
||||||
|
app.use('/api/v1/playout', playoutRouter);
|
||||||
app.use('/api/v1/settings', settingsRouter);
|
app.use('/api/v1/settings', settingsRouter);
|
||||||
app.use('/api/v1/ampp', amppRouter);
|
app.use('/api/v1/ampp', amppRouter);
|
||||||
app.use('/api/v1/groups', requireAdmin, groupsRouter);
|
app.use('/api/v1/groups', requireAdmin, groupsRouter);
|
||||||
|
|
@ -145,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; }
|
||||||
|
|
@ -172,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';
|
||||||
|
|
||||||
|
|
@ -198,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);
|
||||||
}
|
}
|
||||||
|
|
@ -207,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();
|
||||||
|
|
@ -225,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(
|
||||||
|
|
@ -249,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();
|
||||||
|
|
@ -260,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(),
|
||||||
|
|
@ -292,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');
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,23 @@
|
||||||
|
import crypto from 'crypto';
|
||||||
import pool from '../db/pool.js';
|
import pool from '../db/pool.js';
|
||||||
import { parseBearer, hashToken } from '../auth/tokens.js';
|
import { parseBearer, hashToken } from '../auth/tokens.js';
|
||||||
|
|
||||||
|
// In-process service token for the scheduler's loopback self-calls
|
||||||
|
// (scheduler.js -> /recorders|/playout). The scheduler runs in THIS process, so
|
||||||
|
// a per-boot random constant needs no env/compose config and is never exposed:
|
||||||
|
// it only travels over the loopback fetch inside the same process. Multi-replica
|
||||||
|
// is safe because each replica's scheduler only ever calls 127.0.0.1 (itself),
|
||||||
|
// matching that replica's token. Requests bearing it are treated as the seeded
|
||||||
|
// admin (DEV_USER) so RBAC + FK-bearing routes work.
|
||||||
|
export const INTERNAL_TOKEN = crypto.randomBytes(32).toString('hex');
|
||||||
|
const INTERNAL_HEADER = 'x-internal-token';
|
||||||
|
|
||||||
|
function isInternalCall(req) {
|
||||||
|
const got = req.headers[INTERNAL_HEADER];
|
||||||
|
if (typeof got !== 'string' || got.length !== INTERNAL_TOKEN.length) return false;
|
||||||
|
return crypto.timingSafeEqual(Buffer.from(got), Buffer.from(INTERNAL_TOKEN));
|
||||||
|
}
|
||||||
|
|
||||||
// Stable UUID matching migration 023's seeded dev user.
|
// Stable UUID matching migration 023's seeded dev user.
|
||||||
/** UUID of the seeded dev-mode placeholder. NOT a sentinel for "any unauthenticated user". */
|
/** UUID of the seeded dev-mode placeholder. NOT a sentinel for "any unauthenticated user". */
|
||||||
export const DEV_USER_ID = '00000000-0000-4000-8000-000000000000';
|
export const DEV_USER_ID = '00000000-0000-4000-8000-000000000000';
|
||||||
|
|
@ -25,6 +42,13 @@ async function loadUser(id) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function requireAuth(req, res, next) {
|
export async function requireAuth(req, res, next) {
|
||||||
|
// Internal loopback self-call (scheduler). Acts as the seeded admin so RBAC
|
||||||
|
// and FK-bearing routes work, regardless of AUTH_ENABLED.
|
||||||
|
if (isInternalCall(req)) {
|
||||||
|
req.user = DEV_USER;
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
// Dev mode — attach the seeded dev user so FK-bearing routes work.
|
// Dev mode — attach the seeded dev user so FK-bearing routes work.
|
||||||
if (process.env.AUTH_ENABLED !== 'true') {
|
if (process.env.AUTH_ENABLED !== 'true') {
|
||||||
req.user = DEV_USER;
|
req.user = DEV_USER;
|
||||||
|
|
@ -98,6 +122,8 @@ const CSRF_EXEMPT_PATHS = new Set(['/cluster/heartbeat']);
|
||||||
|
|
||||||
export function requireUiHeader(req, res, next) {
|
export function requireUiHeader(req, res, next) {
|
||||||
if (!MUTATING.has(req.method)) return next();
|
if (!MUTATING.has(req.method)) return next();
|
||||||
|
// Internal loopback self-call (scheduler) — not a browser, can't be drive-by'd.
|
||||||
|
if (isInternalCall(req)) return next();
|
||||||
// Bearer-authed requests (Premiere panel, scripts) are exempt — they're not
|
// Bearer-authed requests (Premiere panel, scripts) are exempt — they're not
|
||||||
// browsers and can't be drive-by'd from another origin.
|
// browsers and can't be drive-by'd from another origin.
|
||||||
if (req.headers.authorization?.toLowerCase().startsWith('bearer ')) return next();
|
if (req.headers.authorization?.toLowerCase().startsWith('bearer ')) return next();
|
||||||
|
|
|
||||||
|
|
@ -734,11 +734,14 @@ router.get('/:id/live-path', async (req, res, next) => {
|
||||||
if (a.rows.length === 0) return res.status(404).json({ error: 'Asset not found' });
|
if (a.rows.length === 0) return res.status(404).json({ error: 'Asset not found' });
|
||||||
const asset = a.rows[0];
|
const asset = a.rows[0];
|
||||||
if (asset.status !== 'live') return res.status(409).json({ error: 'Asset is not currently growing', status: asset.status });
|
if (asset.status !== 'live') return res.status(409).json({ error: 'Asset is not currently growing', status: asset.status });
|
||||||
const s = await pool.query(`SELECT key, value FROM settings WHERE key IN ('growing_enabled','growing_smb_url')`);
|
// Growing-files mode is now per-recorder (recorders.growing_enabled), so we
|
||||||
|
// no longer gate on the removed global `growing_enabled` setting. A
|
||||||
|
// status='live' asset already proves a growing recorder is producing this
|
||||||
|
// file; we only need the editor-facing SMB URL to build the UNC path.
|
||||||
|
const s = await pool.query(`SELECT key, value FROM settings WHERE key = 'growing_smb_url'`);
|
||||||
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_enabled !== 'true') return res.status(409).json({ error: 'Growing-files mode is disabled' });
|
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 growing_smb_url in Settings' });
|
|
||||||
const rec = await pool.query(
|
const rec = await pool.query(
|
||||||
`SELECT recording_container FROM recorders WHERE current_session_id = $1 ORDER BY updated_at DESC LIMIT 1`,
|
`SELECT recording_container FROM recorders WHERE current_session_id = $1 ORDER BY updated_at DESC LIMIT 1`,
|
||||||
[asset.id]
|
[asset.id]
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,23 @@
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import http from 'http';
|
import http from 'http';
|
||||||
import pool from '../db/pool.js';
|
import pool from '../db/pool.js';
|
||||||
|
import { requireAdmin } from '../middleware/auth.js';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
// GET /onboard-info – admin-only. Supplies the Add Node wizard with the bits it
|
||||||
|
// needs to build a `curl … | bash` onboarding command: the primary API URL the
|
||||||
|
// remote node-agent should heartbeat to, the raw URL of onboard-node.sh, and
|
||||||
|
// the deploy branch. apiUrl is a best guess the UI lets the operator edit.
|
||||||
|
router.get('/onboard-info', requireAdmin, (req, res) => {
|
||||||
|
const branch = process.env.DEPLOY_BRANCH || 'main';
|
||||||
|
const apiUrl = process.env.PUBLIC_API_URL
|
||||||
|
|| `${req.protocol}://${req.hostname}:${process.env.API_PORT || 47432}`;
|
||||||
|
const scriptUrl =
|
||||||
|
`https://forge.wilddragon.net/zgaetano/wild-dragon/raw/branch/${branch}/deploy/onboard-node.sh`;
|
||||||
|
res.json({ apiUrl, scriptUrl, branch });
|
||||||
|
});
|
||||||
|
|
||||||
// If the agent reported Docker's default bridge IP (172.17.x) but the request
|
// If the agent reported Docker's default bridge IP (172.17.x) but the request
|
||||||
// itself came from a real LAN address, prefer the request source IP instead.
|
// itself came from a real LAN address, prefer the request source IP instead.
|
||||||
// We only check 172.17.x — the default docker0 bridge — not the full RFC1918
|
// We only check 172.17.x — the default docker0 bridge — not the full RFC1918
|
||||||
|
|
@ -41,7 +55,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(
|
||||||
|
|
@ -57,7 +70,6 @@ router.get('/', async (req, res, next) => {
|
||||||
} 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');
|
||||||
|
|
@ -88,7 +100,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');
|
||||||
|
|
@ -96,7 +107,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 {
|
||||||
|
|
@ -108,11 +118,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) {
|
||||||
|
|
@ -132,8 +137,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,
|
||||||
|
|
@ -143,6 +148,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)
|
||||||
|
|
@ -165,42 +171,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;
|
||||||
|
|
@ -208,79 +197,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) {
|
||||||
|
|
@ -288,157 +249,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 });
|
||||||
|
|
@ -448,8 +350,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(
|
||||||
|
|
@ -457,59 +434,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); }
|
||||||
|
|
|
||||||
|
|
@ -22,20 +22,22 @@ const parseRedisUrl = (url) => {
|
||||||
|
|
||||||
const redisConn = parseRedisUrl(process.env.REDIS_URL || 'redis://queue:6379');
|
const redisConn = parseRedisUrl(process.env.REDIS_URL || 'redis://queue:6379');
|
||||||
|
|
||||||
const proxyQueue = new Queue('proxy', { connection: redisConn });
|
const proxyQueue = new Queue('proxy', { connection: redisConn });
|
||||||
const thumbnailQueue = new Queue('thumbnail', { connection: redisConn });
|
const thumbnailQueue = new Queue('thumbnail', { connection: redisConn });
|
||||||
const filmstripQueue = new Queue('filmstrip', { connection: redisConn });
|
const filmstripQueue = new Queue('filmstrip', { connection: redisConn });
|
||||||
const conformQueue = new Queue('conform', { connection: redisConn });
|
const conformQueue = new Queue('conform', { connection: redisConn });
|
||||||
const importQueue = new Queue('import', { connection: redisConn });
|
const importQueue = new Queue('import', { connection: redisConn });
|
||||||
const trimQueue = new Queue('trim', { connection: redisConn });
|
const trimQueue = new Queue('trim', { connection: redisConn });
|
||||||
|
const playoutStageQueue = new Queue('playout-stage', { connection: redisConn });
|
||||||
|
|
||||||
const QUEUES = [
|
const QUEUES = [
|
||||||
{ queue: proxyQueue, type: 'proxy' },
|
{ queue: proxyQueue, type: 'proxy' },
|
||||||
{ queue: thumbnailQueue, type: 'thumbnail' },
|
{ queue: thumbnailQueue, type: 'thumbnail' },
|
||||||
{ queue: filmstripQueue, type: 'filmstrip' },
|
{ queue: filmstripQueue, type: 'filmstrip' },
|
||||||
{ queue: conformQueue, type: 'conform' },
|
{ queue: conformQueue, type: 'conform' },
|
||||||
{ queue: importQueue, type: 'import' },
|
{ queue: importQueue, type: 'import' },
|
||||||
{ queue: trimQueue, type: 'trim' },
|
{ queue: trimQueue, type: 'trim' },
|
||||||
|
{ queue: playoutStageQueue, type: 'playout-stage' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// BullMQ state → API status mapping
|
// BullMQ state → API status mapping
|
||||||
|
|
|
||||||
666
services/mam-api/src/routes/playout.js
Normal file
666
services/mam-api/src/routes/playout.js
Normal file
|
|
@ -0,0 +1,666 @@
|
||||||
|
// Playout / Master Control routes.
|
||||||
|
//
|
||||||
|
// Control plane for the CasparCG-backed playout subsystem. Channels are placed
|
||||||
|
// on cluster nodes and their engine containers spawned via the same Docker-socket
|
||||||
|
// / node-agent path recorders use; the channel's transport (play / pause / skip)
|
||||||
|
// is proxied through to the sidecar's HTTP shim, which drives CasparCG over AMCP.
|
||||||
|
//
|
||||||
|
// RBAC: every channel carries a project_id (NULL = admin-only, the recorder
|
||||||
|
// convention). List routes filter by accessible projects; mutating routes assert
|
||||||
|
// 'edit'. See docs/superpowers/specs/2026-05-30-playout-mcr-design.md.
|
||||||
|
|
||||||
|
import express from 'express';
|
||||||
|
import http from 'http';
|
||||||
|
import { readFile } from 'fs/promises';
|
||||||
|
import { Queue } from 'bullmq';
|
||||||
|
import pool from '../db/pool.js';
|
||||||
|
import { validateUuid } from '../middleware/errors.js';
|
||||||
|
import {
|
||||||
|
assertProjectAccess, accessibleProjectIds, isAdmin,
|
||||||
|
} from '../auth/authz.js';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
const parseRedisUrl = (url) => {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
return { host: parsed.hostname, port: parseInt(parsed.port, 10) };
|
||||||
|
};
|
||||||
|
const stageQueue = new Queue('playout-stage', {
|
||||||
|
connection: parseRedisUrl(process.env.REDIS_URL || 'redis://queue:6379'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const PLAYOUT_SIDECAR_IMAGE = process.env.PLAYOUT_IMAGE || 'wild-dragon-playout:latest';
|
||||||
|
|
||||||
|
function dockerApi(method, path, body = null) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const options = {
|
||||||
|
socketPath: '/var/run/docker.sock',
|
||||||
|
path: `/v1.43${path}`,
|
||||||
|
method,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
};
|
||||||
|
const req = http.request(options, (res) => {
|
||||||
|
let data = '';
|
||||||
|
res.on('data', (chunk) => (data += chunk));
|
||||||
|
res.on('end', () => {
|
||||||
|
try { resolve({ status: res.statusCode, data: data ? JSON.parse(data) : {} }); }
|
||||||
|
catch { resolve({ status: res.statusCode, data }); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
req.on('error', reject);
|
||||||
|
req.setTimeout(10000, () => req.destroy(new Error('Docker API timeout after 10s')));
|
||||||
|
if (body) req.write(JSON.stringify(body));
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveNodeTarget(nodeId) {
|
||||||
|
if (!nodeId) return { remote: false };
|
||||||
|
const r = await pool.query(
|
||||||
|
'SELECT hostname, ip_address, api_url FROM cluster_nodes WHERE id = $1', [nodeId]
|
||||||
|
);
|
||||||
|
if (r.rows.length === 0) return { remote: false };
|
||||||
|
const node = r.rows[0];
|
||||||
|
const localHostname = process.env.NODE_HOSTNAME || '';
|
||||||
|
if (!node.api_url || node.hostname === localHostname) return { remote: false };
|
||||||
|
return { remote: true, apiUrl: node.api_url, ip: node.ip_address };
|
||||||
|
}
|
||||||
|
|
||||||
|
const SIDECAR_HTTP_PORT = 3002;
|
||||||
|
|
||||||
|
function channelAlias(id) { return `playout-${id}`; }
|
||||||
|
|
||||||
|
function sidecarBaseUrl(channel) {
|
||||||
|
if (channel.container_meta && channel.container_meta.sidecar_url) {
|
||||||
|
return channel.container_meta.sidecar_url;
|
||||||
|
}
|
||||||
|
return `http://${channelAlias(channel.id)}:${SIDECAR_HTTP_PORT}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function callSidecar(channel, path, method = 'POST', body = null) {
|
||||||
|
const url = `${sidecarBaseUrl(channel)}${path}`;
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
signal: AbortSignal.timeout(20000),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const text = await res.text().catch(() => '');
|
||||||
|
throw new Error(`sidecar ${method} ${path} -> HTTP ${res.status}: ${text.slice(0, 200)}`);
|
||||||
|
}
|
||||||
|
return res.json().catch(() => ({}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function channelToJson(r) {
|
||||||
|
return {
|
||||||
|
id: r.id,
|
||||||
|
name: r.name,
|
||||||
|
node_id: r.node_id,
|
||||||
|
output_type: r.output_type,
|
||||||
|
output_config: r.output_config,
|
||||||
|
video_format: r.video_format,
|
||||||
|
status: r.status,
|
||||||
|
container_id: r.container_id,
|
||||||
|
error_message: r.error_message,
|
||||||
|
project_id: r.project_id,
|
||||||
|
restart_count: r.restart_count ?? 0,
|
||||||
|
last_restart_at: r.last_restart_at,
|
||||||
|
last_heartbeat_at: r.last_heartbeat_at,
|
||||||
|
created_at: r.created_at,
|
||||||
|
updated_at: r.updated_at,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const OUTPUT_TYPES = new Set(['decklink', 'ndi', 'srt', 'rtmp']);
|
||||||
|
|
||||||
|
router.param('id', async (req, res, next) => {
|
||||||
|
validateUuid('id')(req, res, () => {});
|
||||||
|
if (res.headersSent) return;
|
||||||
|
try {
|
||||||
|
const { rows } = await pool.query(
|
||||||
|
'SELECT * FROM playout_channels WHERE id = $1', [req.params.id]
|
||||||
|
);
|
||||||
|
if (rows.length === 0) return res.status(404).json({ error: 'Channel not found' });
|
||||||
|
req.channel = rows[0];
|
||||||
|
await assertProjectAccess(req.user, req.channel.project_id, 'view');
|
||||||
|
next();
|
||||||
|
} catch (err) { next(err); }
|
||||||
|
});
|
||||||
|
|
||||||
|
async function requireChannelEdit(req, res, next) {
|
||||||
|
try { await assertProjectAccess(req.user, req.channel.project_id, 'edit'); next(); }
|
||||||
|
catch (err) { next(err); }
|
||||||
|
}
|
||||||
|
|
||||||
|
router.get('/channels', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
let rows;
|
||||||
|
if (isAdmin(req.user)) {
|
||||||
|
({ rows } = await pool.query('SELECT * FROM playout_channels ORDER BY created_at DESC'));
|
||||||
|
} else {
|
||||||
|
const ids = await accessibleProjectIds(req.user);
|
||||||
|
if (ids.length === 0) return res.json([]);
|
||||||
|
({ rows } = await pool.query(
|
||||||
|
'SELECT * FROM playout_channels WHERE project_id = ANY($1) ORDER BY created_at DESC', [ids]
|
||||||
|
));
|
||||||
|
}
|
||||||
|
res.json(rows.map(channelToJson));
|
||||||
|
} catch (err) { next(err); }
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/channels', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { name, node_id = null, output_type = 'srt', output_config = {},
|
||||||
|
video_format = '1080p5994', project_id = null } = req.body || {};
|
||||||
|
if (!name || typeof name !== 'string') {
|
||||||
|
return res.status(400).json({ error: 'name is required' });
|
||||||
|
}
|
||||||
|
if (!OUTPUT_TYPES.has(output_type)) {
|
||||||
|
return res.status(400).json({ error: `output_type must be one of: ${[...OUTPUT_TYPES].join(', ')}` });
|
||||||
|
}
|
||||||
|
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' });
|
||||||
|
|
||||||
|
const { rows } = await pool.query(
|
||||||
|
`INSERT INTO playout_channels (name, node_id, output_type, output_config, video_format, project_id)
|
||||||
|
VALUES ($1,$2,$3,$4,$5,$6) RETURNING *`,
|
||||||
|
[name.trim(), node_id, output_type, JSON.stringify(output_config), video_format, project_id]
|
||||||
|
);
|
||||||
|
res.status(201).json(channelToJson(rows[0]));
|
||||||
|
} catch (err) { next(err); }
|
||||||
|
});
|
||||||
|
|
||||||
|
router.patch('/channels/:id', requireChannelEdit, async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
if (req.channel.status === 'running') {
|
||||||
|
return res.status(409).json({ error: 'Cannot edit a running channel — stop it first' });
|
||||||
|
}
|
||||||
|
const allowed = ['name', 'node_id', 'output_type', 'output_config', 'video_format', 'project_id'];
|
||||||
|
const sets = [];
|
||||||
|
const vals = [];
|
||||||
|
let i = 1;
|
||||||
|
for (const k of allowed) {
|
||||||
|
if (req.body[k] === undefined) continue;
|
||||||
|
if (k === 'output_type' && !OUTPUT_TYPES.has(req.body[k])) {
|
||||||
|
return res.status(400).json({ error: 'invalid output_type' });
|
||||||
|
}
|
||||||
|
sets.push(`${k} = $${i++}`);
|
||||||
|
vals.push(k === 'output_config' ? JSON.stringify(req.body[k]) : req.body[k]);
|
||||||
|
}
|
||||||
|
if (sets.length === 0) return res.json(channelToJson(req.channel));
|
||||||
|
vals.push(req.channel.id);
|
||||||
|
const { rows } = await pool.query(
|
||||||
|
`UPDATE playout_channels SET ${sets.join(', ')}, updated_at = NOW() WHERE id = $${i} RETURNING *`, vals
|
||||||
|
);
|
||||||
|
res.json(channelToJson(rows[0]));
|
||||||
|
} catch (err) { next(err); }
|
||||||
|
});
|
||||||
|
|
||||||
|
router.delete('/channels/:id', requireChannelEdit, async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
if (req.channel.status === 'running') {
|
||||||
|
return res.status(409).json({ error: 'Stop the channel before deleting it' });
|
||||||
|
}
|
||||||
|
await pool.query('DELETE FROM playout_channels WHERE id = $1', [req.channel.id]);
|
||||||
|
res.json({ deleted: true });
|
||||||
|
} catch (err) { next(err); }
|
||||||
|
});
|
||||||
|
|
||||||
|
async function assertDeckLinkFree(channel) {
|
||||||
|
if (channel.output_type !== 'decklink') return;
|
||||||
|
const idx = (channel.output_config && channel.output_config.device_index) || 1;
|
||||||
|
const chan = await pool.query(
|
||||||
|
`SELECT id FROM playout_channels
|
||||||
|
WHERE id <> $1 AND node_id IS NOT DISTINCT FROM $2 AND status = 'running'
|
||||||
|
AND output_type = 'decklink' AND (output_config->>'device_index')::int = $3`,
|
||||||
|
[channel.id, channel.node_id, idx]
|
||||||
|
);
|
||||||
|
if (chan.rows.length > 0) {
|
||||||
|
throw Object.assign(new Error(`DeckLink device ${idx} already in use by another channel on this node`), { httpStatus: 409 });
|
||||||
|
}
|
||||||
|
const rec = await pool.query(
|
||||||
|
`SELECT id FROM recorders
|
||||||
|
WHERE node_id IS NOT DISTINCT FROM $1 AND device_index = $2
|
||||||
|
AND status = 'recording' AND source_type = 'sdi'`,
|
||||||
|
[channel.node_id, idx]
|
||||||
|
);
|
||||||
|
if (rec.rows.length > 0) {
|
||||||
|
throw Object.assign(new Error(`DeckLink device ${idx} is in use by a recorder on this node`), { httpStatus: 409 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function spawnChannelSidecar(channel) {
|
||||||
|
await pool.query('UPDATE playout_channels SET status = $1, error_message = NULL WHERE id = $2', ['starting', channel.id]);
|
||||||
|
|
||||||
|
const env = [
|
||||||
|
`OUTPUT_TYPE=${channel.output_type}`,
|
||||||
|
`OUTPUT_CONFIG=${JSON.stringify(channel.output_config || {})}`,
|
||||||
|
`VIDEO_FORMAT=${channel.video_format}`,
|
||||||
|
`PORT=${SIDECAR_HTTP_PORT}`,
|
||||||
|
`CHANNEL_ID=${channel.id}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
const { remote: isRemote, apiUrl: targetNodeApiUrl } = await resolveNodeTarget(channel.node_id);
|
||||||
|
const dockerNetwork = process.env.DOCKER_NETWORK || 'wild-dragon_wild-dragon';
|
||||||
|
let containerId;
|
||||||
|
let containerMeta = {};
|
||||||
|
|
||||||
|
if (isRemote) {
|
||||||
|
const sidecarRes = await fetch(`${targetNodeApiUrl}/sidecar/start`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
image: PLAYOUT_SIDECAR_IMAGE, env,
|
||||||
|
capturePort: SIDECAR_HTTP_PORT,
|
||||||
|
sourceType: channel.output_type,
|
||||||
|
useGpu: false,
|
||||||
|
publishHttp: true,
|
||||||
|
}),
|
||||||
|
signal: AbortSignal.timeout(20000),
|
||||||
|
});
|
||||||
|
if (!sidecarRes.ok) {
|
||||||
|
const details = await sidecarRes.json().catch(() => ({}));
|
||||||
|
console.error('[playout] remote sidecar start failed:', JSON.stringify(details));
|
||||||
|
await pool.query('UPDATE playout_channels SET status = $1, error_message = $2 WHERE id = $3',
|
||||||
|
['error', 'remote node failed to start sidecar', channel.id]);
|
||||||
|
throw Object.assign(new Error('Remote node failed to start sidecar'), { httpStatus: 502 });
|
||||||
|
}
|
||||||
|
const data = await sidecarRes.json();
|
||||||
|
containerId = data.containerId;
|
||||||
|
if (data.sidecarUrl || data.host) {
|
||||||
|
containerMeta.sidecar_url = data.sidecarUrl || `http://${data.host}:${SIDECAR_HTTP_PORT}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const alias = channelAlias(channel.id);
|
||||||
|
const hostBinds = ['/mnt/NVME/MAM/wild-dragon-media:/media'];
|
||||||
|
if (channel.output_type === 'decklink') hostBinds.push('/dev/blackmagic:/dev/blackmagic');
|
||||||
|
|
||||||
|
const containerConfig = {
|
||||||
|
Image: PLAYOUT_SIDECAR_IMAGE,
|
||||||
|
Env: env,
|
||||||
|
HostConfig: {
|
||||||
|
// DeckLink SDI needs raw /dev access (privileged). SRT/NDI/RTMP/HLS run
|
||||||
|
// unprivileged — privileged exposes host GPUs to CasparCG, and the
|
||||||
|
// missing in-container NVIDIA driver crashes the engine within seconds.
|
||||||
|
Privileged: channel.output_type === 'decklink',
|
||||||
|
NetworkMode: dockerNetwork,
|
||||||
|
Binds: hostBinds,
|
||||||
|
},
|
||||||
|
NetworkingConfig: { EndpointsConfig: { [dockerNetwork]: { Aliases: [alias] } } },
|
||||||
|
Hostname: alias,
|
||||||
|
};
|
||||||
|
const createRes = await dockerApi('POST', '/containers/create', containerConfig);
|
||||||
|
if (createRes.status !== 201) {
|
||||||
|
console.error('[playout] container create failed:', JSON.stringify(createRes.data));
|
||||||
|
await pool.query('UPDATE playout_channels SET status = $1, error_message = $2 WHERE id = $3',
|
||||||
|
['error', 'container create failed', channel.id]);
|
||||||
|
throw Object.assign(new Error('Failed to create container'), { httpStatus: 500 });
|
||||||
|
}
|
||||||
|
containerId = createRes.data.Id;
|
||||||
|
const startRes = await dockerApi('POST', `/containers/${containerId}/start`);
|
||||||
|
if (startRes.status !== 204) {
|
||||||
|
console.error('[playout] container start failed:', JSON.stringify(startRes.data));
|
||||||
|
await dockerApi('DELETE', `/containers/${containerId}?force=true`).catch(() => {});
|
||||||
|
await pool.query('UPDATE playout_channels SET status = $1, error_message = $2 WHERE id = $3',
|
||||||
|
['error', 'container start failed', channel.id]);
|
||||||
|
throw Object.assign(new Error('Failed to start container'), { httpStatus: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set last_heartbeat_at = NOW() so the scheduler health tick treats this
|
||||||
|
// channel as freshly alive. Without this, last_heartbeat_at starts as NULL
|
||||||
|
// (epoch=0), and the very first tick sees ageMs >> TIMEOUT_MS and triggers
|
||||||
|
// failover immediately — before the sidecar has had a chance to respond.
|
||||||
|
const { rows } = await pool.query(
|
||||||
|
`UPDATE playout_channels
|
||||||
|
SET status = 'running', container_id = $1, container_meta = $2,
|
||||||
|
last_heartbeat_at = NOW(), updated_at = NOW()
|
||||||
|
WHERE id = $3 RETURNING *`,
|
||||||
|
[containerId, JSON.stringify(containerMeta), channel.id]
|
||||||
|
);
|
||||||
|
return rows[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
router.post('/channels/:id/start', requireChannelEdit, async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const channel = req.channel;
|
||||||
|
if (channel.status === 'running' || channel.status === 'starting') {
|
||||||
|
return res.status(409).json({ error: `Channel already ${channel.status}` });
|
||||||
|
}
|
||||||
|
await assertDeckLinkFree(channel);
|
||||||
|
const row = await spawnChannelSidecar(channel);
|
||||||
|
res.json(channelToJson(row));
|
||||||
|
} catch (err) {
|
||||||
|
if (err.httpStatus) return res.status(err.httpStatus).json({ error: err.message });
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/channels/:id/stop', requireChannelEdit, async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const channel = req.channel;
|
||||||
|
if (channel.container_id) {
|
||||||
|
const { remote: isRemote, apiUrl } = await resolveNodeTarget(channel.node_id);
|
||||||
|
if (isRemote) {
|
||||||
|
await fetch(`${apiUrl}/sidecar/stop`, {
|
||||||
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ containerId: channel.container_id }),
|
||||||
|
signal: AbortSignal.timeout(20000),
|
||||||
|
}).catch((e) => console.error('[playout] remote stop failed:', e.message));
|
||||||
|
} else {
|
||||||
|
await dockerApi('POST', `/containers/${channel.container_id}/stop?t=10`).catch(() => {});
|
||||||
|
await dockerApi('DELETE', `/containers/${channel.container_id}?force=true`).catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const { rows } = await pool.query(
|
||||||
|
`UPDATE playout_channels SET status = 'stopped', container_id = NULL, updated_at = NOW()
|
||||||
|
WHERE id = $1 RETURNING *`, [channel.id]
|
||||||
|
);
|
||||||
|
res.json(channelToJson(rows[0]));
|
||||||
|
} catch (err) { next(err); }
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/channels/:id/status', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
if (req.channel.status !== 'running') {
|
||||||
|
return res.json({ running: false, status: req.channel.status });
|
||||||
|
}
|
||||||
|
const out = await callSidecar(req.channel, '/status', 'GET');
|
||||||
|
res.json({ running: true, status: req.channel.status, engine: out });
|
||||||
|
} catch (err) {
|
||||||
|
res.json({ running: true, status: req.channel.status, engine: null, engine_error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /playout/channels/:id/hls/index.m3u8 — the live preview playlist, served
|
||||||
|
// through the API (not the static /media/live path) so it bypasses the public
|
||||||
|
// reverse proxy's static cache. That proxy caches the .m3u8 by path with a
|
||||||
|
// multi-second TTL and ignores the origin's no-store, so hls.js's ~1s reloads
|
||||||
|
// always got a STALE playlist ("MISSED" forever → monitor stayed black). The
|
||||||
|
// /api/ path is not proxy-cached (the status poll updates fine), so this always
|
||||||
|
// returns the fresh live edge. Segment (.ts) lines are rewritten to absolute
|
||||||
|
// /media/live/<id>/ URLs so they still load from the static path (immutable,
|
||||||
|
// caching them is fine). mam-api shares the same /media volume the sidecars
|
||||||
|
// write to.
|
||||||
|
const HLS_MEDIA_DIR = process.env.PLAYOUT_MEDIA_DIR || '/media';
|
||||||
|
router.get('/channels/:id/hls/index.m3u8', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const cid = req.channel.id;
|
||||||
|
let body;
|
||||||
|
try {
|
||||||
|
body = await readFile(`${HLS_MEDIA_DIR}/live/${cid}/index.m3u8`, 'utf8');
|
||||||
|
} catch (e) {
|
||||||
|
return res.status(404).json({ error: 'No live preview for this channel yet' });
|
||||||
|
}
|
||||||
|
// Rewrite bare segment names to absolute static URLs.
|
||||||
|
const rewritten = body
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => (/^[^#].*\.ts\s*$/.test(line) ? `/media/live/${cid}/${line.trim()}` : line))
|
||||||
|
.join('\n');
|
||||||
|
res.setHeader('Content-Type', 'application/vnd.apple.mpegurl');
|
||||||
|
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
|
||||||
|
res.setHeader('Pragma', 'no-cache');
|
||||||
|
res.send(rewritten);
|
||||||
|
} catch (err) { next(err); }
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Transport ────────────────────────────────────────────────────────────────
|
||||||
|
async function transport(req, res, action, body = null) {
|
||||||
|
if (req.channel.status !== 'running') {
|
||||||
|
return res.status(409).json({ error: 'Channel is not running' });
|
||||||
|
}
|
||||||
|
try { res.json(await callSidecar(req.channel, action, 'POST', body)); }
|
||||||
|
catch (err) { res.status(502).json({ error: err.message }); }
|
||||||
|
}
|
||||||
|
|
||||||
|
router.post('/channels/:id/play', requireChannelEdit, async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
if (req.channel.status !== 'running') {
|
||||||
|
return res.status(409).json({ error: 'Start the channel before playing' });
|
||||||
|
}
|
||||||
|
const { playlist_id } = req.body || {};
|
||||||
|
if (!playlist_id) return res.status(400).json({ error: 'playlist_id is required' });
|
||||||
|
|
||||||
|
const pl = await pool.query('SELECT * FROM playout_playlists WHERE id = $1 AND channel_id = $2',
|
||||||
|
[playlist_id, req.channel.id]);
|
||||||
|
if (pl.rows.length === 0) return res.status(404).json({ error: 'Playlist not found for this channel' });
|
||||||
|
|
||||||
|
const items = await pool.query(
|
||||||
|
`SELECT i.*, a.filename AS clip_name, a.duration_ms AS asset_duration_ms
|
||||||
|
FROM playout_items i JOIN assets a ON a.id = i.asset_id
|
||||||
|
WHERE i.playlist_id = $1 ORDER BY i.sort_order ASC`, [playlist_id]);
|
||||||
|
|
||||||
|
const notReady = items.rows.filter((i) => i.media_status !== 'ready' || !i.media_path);
|
||||||
|
if (notReady.length > 0) {
|
||||||
|
return res.status(409).json({
|
||||||
|
error: 'Some items are not staged yet',
|
||||||
|
pending: notReady.map((i) => i.id),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
loop: pl.rows[0].loop,
|
||||||
|
items: items.rows.map((i) => ({
|
||||||
|
id: i.id, asset_id: i.asset_id, media_path: i.media_path,
|
||||||
|
in_point: i.in_point ? Number(i.in_point) : null,
|
||||||
|
out_point: i.out_point ? Number(i.out_point) : null,
|
||||||
|
transition: i.transition, transition_ms: i.transition_ms,
|
||||||
|
clip_name: i.clip_name,
|
||||||
|
asset_duration_ms: i.asset_duration_ms != null ? Number(i.asset_duration_ms) : null,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
// callSidecar throws on network/timeout errors. Return 502 (not 409) so
|
||||||
|
// the UI and operators know it's a gateway problem, not a state conflict.
|
||||||
|
let out;
|
||||||
|
try {
|
||||||
|
out = await callSidecar(req.channel, '/playlist/load', 'POST', payload);
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(502).json({ error: 'Sidecar unreachable: ' + err.message });
|
||||||
|
}
|
||||||
|
res.json(out);
|
||||||
|
} catch (err) { next(err); }
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/channels/:id/pause', requireChannelEdit, (req, res) => transport(req, res, '/transport/pause'));
|
||||||
|
router.post('/channels/:id/resume', requireChannelEdit, (req, res) => transport(req, res, '/transport/resume'));
|
||||||
|
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.get('/channels/:id/asrun', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { rows } = await pool.query(
|
||||||
|
`SELECT * FROM playout_as_run WHERE channel_id = $1 ORDER BY started_at DESC LIMIT 500`,
|
||||||
|
[req.channel.id]);
|
||||||
|
res.json(rows);
|
||||||
|
} catch (err) { next(err); }
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadChannelForBody(req, res, next) {
|
||||||
|
const channelId = req.body.channel_id || req.query.channel_id;
|
||||||
|
if (!channelId) return res.status(400).json({ error: 'channel_id is required' });
|
||||||
|
try {
|
||||||
|
const { rows } = await pool.query('SELECT * FROM playout_channels WHERE id = $1', [channelId]);
|
||||||
|
if (rows.length === 0) return res.status(404).json({ error: 'Channel not found' });
|
||||||
|
req.channel = rows[0];
|
||||||
|
await assertProjectAccess(req.user, req.channel.project_id, 'edit');
|
||||||
|
next();
|
||||||
|
} catch (err) { next(err); }
|
||||||
|
}
|
||||||
|
|
||||||
|
router.get('/playlists', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const channelId = req.query.channel_id;
|
||||||
|
if (!channelId) return res.status(400).json({ error: 'channel_id is required' });
|
||||||
|
const ch = await pool.query('SELECT project_id FROM playout_channels WHERE id = $1', [channelId]);
|
||||||
|
if (ch.rows.length === 0) return res.status(404).json({ error: 'Channel not found' });
|
||||||
|
await assertProjectAccess(req.user, ch.rows[0].project_id, 'view');
|
||||||
|
const { rows } = await pool.query(
|
||||||
|
'SELECT * FROM playout_playlists WHERE channel_id = $1 ORDER BY created_at ASC', [channelId]);
|
||||||
|
res.json(rows);
|
||||||
|
} catch (err) { next(err); }
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/playlists', loadChannelForBody, async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { name, loop = false } = req.body || {};
|
||||||
|
if (!name) return res.status(400).json({ error: 'name is required' });
|
||||||
|
const { rows } = await pool.query(
|
||||||
|
'INSERT INTO playout_playlists (channel_id, name, loop) VALUES ($1,$2,$3) RETURNING *',
|
||||||
|
[req.channel.id, name.trim(), !!loop]);
|
||||||
|
res.status(201).json(rows[0]);
|
||||||
|
} catch (err) { next(err); }
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/playlists/:plid/items', validateUuid('plid'), async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const pl = await pool.query(
|
||||||
|
`SELECT p.*, c.project_id FROM playout_playlists p
|
||||||
|
JOIN playout_channels c ON c.id = p.channel_id WHERE p.id = $1`, [req.params.plid]);
|
||||||
|
if (pl.rows.length === 0) return res.status(404).json({ error: 'Playlist not found' });
|
||||||
|
await assertProjectAccess(req.user, pl.rows[0].project_id, 'view');
|
||||||
|
const { rows } = await pool.query(
|
||||||
|
`SELECT i.*, a.filename AS clip_name, a.duration_ms AS asset_duration_ms
|
||||||
|
FROM playout_items i JOIN assets a ON a.id = i.asset_id
|
||||||
|
WHERE i.playlist_id = $1 ORDER BY i.sort_order ASC`, [req.params.plid]);
|
||||||
|
res.json(rows);
|
||||||
|
} catch (err) { next(err); }
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadPlaylistEdit(plid, user) {
|
||||||
|
const pl = await pool.query(
|
||||||
|
`SELECT p.*, c.project_id FROM playout_playlists p
|
||||||
|
JOIN playout_channels c ON c.id = p.channel_id WHERE p.id = $1`, [plid]);
|
||||||
|
if (pl.rows.length === 0) { throw Object.assign(new Error('Playlist not found'), { httpStatus: 404 }); }
|
||||||
|
await assertProjectAccess(user, pl.rows[0].project_id, 'edit');
|
||||||
|
return pl.rows[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
router.post('/playlists/:plid/items', validateUuid('plid'), async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
await loadPlaylistEdit(req.params.plid, req.user);
|
||||||
|
const { asset_id, in_point = null, out_point = null,
|
||||||
|
transition = 'cut', transition_ms = 0 } = req.body || {};
|
||||||
|
if (!asset_id) return res.status(400).json({ error: 'asset_id is required' });
|
||||||
|
|
||||||
|
const ord = await pool.query(
|
||||||
|
'SELECT COALESCE(MAX(sort_order), -1) + 1 AS next FROM playout_items WHERE playlist_id = $1',
|
||||||
|
[req.params.plid]);
|
||||||
|
const { rows } = await pool.query(
|
||||||
|
`INSERT INTO playout_items (playlist_id, asset_id, sort_order, in_point, out_point, transition, transition_ms)
|
||||||
|
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]);
|
||||||
|
|
||||||
|
await stageQueue.add('stage', { itemId: rows[0].id, assetId: asset_id }).catch((e) =>
|
||||||
|
console.error('[playout] failed to enqueue stage job:', e.message));
|
||||||
|
|
||||||
|
res.status(201).json(rows[0]);
|
||||||
|
} catch (err) {
|
||||||
|
if (err.httpStatus) return res.status(err.httpStatus).json({ error: err.message });
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.put('/playlists/:plid/reorder', validateUuid('plid'), async (req, res, next) => {
|
||||||
|
const client = await pool.connect();
|
||||||
|
try {
|
||||||
|
await loadPlaylistEdit(req.params.plid, req.user);
|
||||||
|
const { order } = req.body || {};
|
||||||
|
if (!Array.isArray(order)) return res.status(400).json({ error: 'order must be an array of item ids' });
|
||||||
|
await client.query('BEGIN');
|
||||||
|
for (let i = 0; i < order.length; i++) {
|
||||||
|
await client.query(
|
||||||
|
'UPDATE playout_items SET sort_order = $1, updated_at = NOW() WHERE id = $2 AND playlist_id = $3',
|
||||||
|
[i, order[i], req.params.plid]);
|
||||||
|
}
|
||||||
|
await client.query('COMMIT');
|
||||||
|
res.json({ reordered: order.length });
|
||||||
|
} catch (err) {
|
||||||
|
await client.query('ROLLBACK').catch(() => {});
|
||||||
|
if (err.httpStatus) return res.status(err.httpStatus).json({ error: err.message });
|
||||||
|
next(err);
|
||||||
|
} finally { client.release(); }
|
||||||
|
});
|
||||||
|
|
||||||
|
router.delete('/items/:itemId', validateUuid('itemId'), async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const it = await pool.query(
|
||||||
|
`SELECT i.id, c.project_id FROM playout_items i
|
||||||
|
JOIN playout_playlists p ON p.id = i.playlist_id
|
||||||
|
JOIN playout_channels c ON c.id = p.channel_id WHERE i.id = $1`, [req.params.itemId]);
|
||||||
|
if (it.rows.length === 0) return res.status(404).json({ error: 'Item not found' });
|
||||||
|
await assertProjectAccess(req.user, it.rows[0].project_id, 'edit');
|
||||||
|
await pool.query('DELETE FROM playout_items WHERE id = $1', [req.params.itemId]);
|
||||||
|
res.json({ deleted: true });
|
||||||
|
} catch (err) { next(err); }
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/items/:itemId/stage', validateUuid('itemId'), async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const it = await pool.query(
|
||||||
|
`SELECT i.id, i.asset_id, c.project_id FROM playout_items i
|
||||||
|
JOIN playout_playlists p ON p.id = i.playlist_id
|
||||||
|
JOIN playout_channels c ON c.id = p.channel_id WHERE i.id = $1`, [req.params.itemId]);
|
||||||
|
if (it.rows.length === 0) return res.status(404).json({ error: 'Item not found' });
|
||||||
|
await assertProjectAccess(req.user, it.rows[0].project_id, 'edit');
|
||||||
|
await pool.query("UPDATE playout_items SET media_status = 'pending' WHERE id = $1", [req.params.itemId]);
|
||||||
|
await stageQueue.add('stage', { itemId: it.rows[0].id, assetId: it.rows[0].asset_id });
|
||||||
|
res.json({ queued: true });
|
||||||
|
} catch (err) { next(err); }
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function restartChannel(channelId) {
|
||||||
|
const { rows } = await pool.query('SELECT * FROM playout_channels WHERE id = $1', [channelId]);
|
||||||
|
if (rows.length === 0) return { restarted: false, reason: 'channel not found' };
|
||||||
|
const channel = rows[0];
|
||||||
|
|
||||||
|
if (channel.output_type === 'decklink') {
|
||||||
|
return { restarted: false, reason: 'decklink channels are alert-only' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channel.container_id) {
|
||||||
|
const { remote, apiUrl } = await resolveNodeTarget(channel.node_id);
|
||||||
|
if (remote && apiUrl) {
|
||||||
|
await fetch(`${apiUrl}/sidecar/stop`, {
|
||||||
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ containerId: channel.container_id }),
|
||||||
|
signal: AbortSignal.timeout(10000),
|
||||||
|
}).catch(() => {});
|
||||||
|
} else {
|
||||||
|
await dockerApi('DELETE', `/containers/${channel.container_id}?force=true`).catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodes = await pool.query(
|
||||||
|
`SELECT id, hostname, api_url, last_seen_at FROM cluster_nodes
|
||||||
|
WHERE id <> $1 AND last_seen_at > NOW() - INTERVAL '60 seconds'
|
||||||
|
ORDER BY last_seen_at DESC LIMIT 1`,
|
||||||
|
[channel.node_id]
|
||||||
|
);
|
||||||
|
if (nodes.rows.length === 0) {
|
||||||
|
await pool.query(
|
||||||
|
"UPDATE playout_channels SET status = 'error', error_message = $1 WHERE id = $2",
|
||||||
|
['no healthy node available for failover', channel.id]
|
||||||
|
);
|
||||||
|
return { restarted: false, reason: 'no eligible node' };
|
||||||
|
}
|
||||||
|
const newNodeId = nodes.rows[0].id;
|
||||||
|
|
||||||
|
const { rows: moved } = await pool.query(
|
||||||
|
`UPDATE playout_channels
|
||||||
|
SET node_id = $1, status = 'stopped', container_id = NULL, container_meta = '{}'::jsonb,
|
||||||
|
restart_count = restart_count + 1, last_restart_at = NOW(),
|
||||||
|
error_message = NULL, updated_at = NOW()
|
||||||
|
WHERE id = $2 RETURNING *`,
|
||||||
|
[newNodeId, channel.id]
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await spawnChannelSidecar(moved[0]);
|
||||||
|
return { restarted: true, new_node_id: newNodeId };
|
||||||
|
} catch (err) {
|
||||||
|
return { restarted: false, reason: `respawn failed: ${err.message}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default router;
|
||||||
|
|
@ -154,6 +154,7 @@ const RECORDER_FIELDS = [
|
||||||
'proxy_audio_codec', 'proxy_audio_bitrate', 'proxy_audio_channels',
|
'proxy_audio_codec', 'proxy_audio_bitrate', 'proxy_audio_channels',
|
||||||
'proxy_container',
|
'proxy_container',
|
||||||
'project_id', 'node_id', 'device_index',
|
'project_id', 'node_id', 'device_index',
|
||||||
|
'growing_enabled',
|
||||||
];
|
];
|
||||||
|
|
||||||
function pickRecorderFields(body) {
|
function pickRecorderFields(body) {
|
||||||
|
|
@ -363,14 +364,25 @@ router.post('/:id/start', requireRecorderEdit, async (req, res, next) => {
|
||||||
const externalMamApiUrl = `http://${process.env.NODE_IP || '172.18.91.200'}:${process.env.PORT_MAM_API || 47432}`;
|
const externalMamApiUrl = `http://${process.env.NODE_IP || '172.18.91.200'}:${process.env.PORT_MAM_API || 47432}`;
|
||||||
const dockerNetwork = process.env.DOCKER_NETWORK || 'wild-dragon_wild-dragon';
|
const dockerNetwork = process.env.DOCKER_NETWORK || 'wild-dragon_wild-dragon';
|
||||||
|
|
||||||
// Growing-files mode is a global setting (settings table). When on, the
|
// Growing-files mode is a PER-RECORDER setting (recorders.growing_enabled).
|
||||||
// capture container writes the master to its /growing/ mount instead of
|
// When on, the capture container writes the master to its /growing/ mount
|
||||||
// streaming it to S3 — Premiere can mount the SMB share and edit it live.
|
// instead of streaming it to S3 — editors can mount the SMB share and cut it
|
||||||
const growingRow = await pool.query(
|
// live. The SMB share itself (mount source + credentials) is shared
|
||||||
`SELECT value FROM settings WHERE key = 'growing_enabled'`
|
// infrastructure configured globally in Settings → Storage.
|
||||||
);
|
const growingEnabled = recorder.growing_enabled === true;
|
||||||
const growingEnabled =
|
|
||||||
growingRow.rows[0]?.value === 'true' || growingRow.rows[0]?.value === true;
|
// Shared growing-files SMB infrastructure (global settings). Used to mount
|
||||||
|
// the CIFS share inside the capture container (services/capture mounts it
|
||||||
|
// with these credentials when GROWING_SMB_MOUNT is set).
|
||||||
|
const growingInfra = {};
|
||||||
|
{
|
||||||
|
const r = await pool.query(
|
||||||
|
`SELECT key, value FROM settings WHERE key = ANY($1)`,
|
||||||
|
[['growing_smb_mount', 'growing_smb_username', 'growing_smb_password', 'growing_smb_vers']]
|
||||||
|
);
|
||||||
|
for (const { key, value } of r.rows) growingInfra[key] = value;
|
||||||
|
}
|
||||||
|
const smbMount = growingEnabled ? (growingInfra.growing_smb_mount || '') : '';
|
||||||
|
|
||||||
// Operator-supplied clip name wins over the auto-timestamped fallback.
|
// Operator-supplied clip name wins over the auto-timestamped fallback.
|
||||||
// The Recorders UI passes this on the start request when the user types
|
// The Recorders UI passes this on the start request when the user types
|
||||||
|
|
@ -455,6 +467,13 @@ router.post('/:id/start', requireRecorderEdit, async (req, res, next) => {
|
||||||
`MAM_API_TOKEN=${process.env.CAPTURE_TOKEN || ''}`,
|
`MAM_API_TOKEN=${process.env.CAPTURE_TOKEN || ''}`,
|
||||||
`GROWING_ENABLED=${growingEnabled ? 'true' : 'false'}`,
|
`GROWING_ENABLED=${growingEnabled ? 'true' : 'false'}`,
|
||||||
`GROWING_PATH=/growing`,
|
`GROWING_PATH=/growing`,
|
||||||
|
// SMB mount details for the in-container CIFS mount (Approach A). Empty
|
||||||
|
// GROWING_SMB_MOUNT → capture falls back to the host-bound /growing volume
|
||||||
|
// (or to S3 streaming if growing isn't enabled).
|
||||||
|
`GROWING_SMB_MOUNT=${smbMount}`,
|
||||||
|
`GROWING_SMB_USERNAME=${growingInfra.growing_smb_username || ''}`,
|
||||||
|
`GROWING_SMB_PASSWORD=${growingInfra.growing_smb_password || ''}`,
|
||||||
|
`GROWING_SMB_VERS=${growingInfra.growing_smb_vers || '3.0'}`,
|
||||||
];
|
];
|
||||||
|
|
||||||
// Deltacast: pass port count so the capture container can enumerate
|
// Deltacast: pass port count so the capture container can enumerate
|
||||||
|
|
@ -530,7 +549,15 @@ router.post('/:id/start', requireRecorderEdit, async (req, res, next) => {
|
||||||
for (const d of dcEntries) hostBinds.push(`/dev/${d}:/dev/${d}`);
|
for (const d of dcEntries) hostBinds.push(`/dev/${d}:/dev/${d}`);
|
||||||
} catch (_) { /* no /dev/deltacast* nodes on this host */ }
|
} catch (_) { /* no /dev/deltacast* nodes on this host */ }
|
||||||
}
|
}
|
||||||
if (growingEnabled) hostBinds.push('/mnt/NVME/MAM/wild-dragon-growing:/growing');
|
// /growing handling:
|
||||||
|
// - SMB mount configured → DON'T host-bind; the capture container mounts
|
||||||
|
// the CIFS share at /growing itself (Approach A). A bind-mount here
|
||||||
|
// would shadow the in-container mount.
|
||||||
|
// - growing on but no SMB mount → legacy host bind-mount fallback.
|
||||||
|
// - growing off → no /growing mount at all.
|
||||||
|
if (growingEnabled && !smbMount) {
|
||||||
|
hostBinds.push('/mnt/NVME/MAM/wild-dragon-growing:/growing');
|
||||||
|
}
|
||||||
|
|
||||||
const localEnv = [...env];
|
const localEnv = [...env];
|
||||||
if (useGpu) {
|
if (useGpu) {
|
||||||
|
|
|
||||||
|
|
@ -258,21 +258,45 @@ router.put('/transcoding', async (req, res, next) => {
|
||||||
// while it's still being written; the promotion worker later moves the
|
// while it's still being written; the promotion worker later moves the
|
||||||
// finalized file to S3 and flips the asset to status='ready'.
|
// finalized file to S3 and flips the asset to status='ready'.
|
||||||
|
|
||||||
const GROWING_KEYS = ['growing_enabled', 'growing_path', 'growing_smb_url', 'growing_promote_after_seconds'];
|
// Growing-files mode is now a PER-RECORDER setting (recorders.growing_enabled);
|
||||||
|
// the legacy global `growing_enabled` key is no longer read at recorder start.
|
||||||
|
// These global keys describe the shared SMB landing-zone infrastructure only:
|
||||||
|
// - growing_path container mount point (default /growing)
|
||||||
|
// - growing_smb_url smb://... display string for editors (Premiere)
|
||||||
|
// - growing_smb_mount //host/share CIFS source the capture container mounts
|
||||||
|
// - growing_smb_username SMB user for the system-side CIFS mount
|
||||||
|
// - growing_smb_password SMB password (WRITE-ONLY; never returned)
|
||||||
|
// - growing_smb_vers CIFS protocol version (default 3.0)
|
||||||
|
// - growing_promote_after_seconds idle threshold before S3 promotion
|
||||||
|
const GROWING_KEYS = [
|
||||||
|
'growing_path', 'growing_smb_url', 'growing_smb_mount',
|
||||||
|
'growing_smb_username', 'growing_smb_vers', 'growing_promote_after_seconds',
|
||||||
|
];
|
||||||
|
// growing_smb_password is handled separately: stored on PUT but NEVER returned
|
||||||
|
// on GET (only a *_exists flag), mirroring s3_secret_key.
|
||||||
|
|
||||||
router.get('/growing', async (req, res, next) => {
|
router.get('/growing', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const result = await pool.query(
|
const result = await pool.query(
|
||||||
`SELECT key, value FROM settings WHERE key = ANY($1)`,
|
`SELECT key, value FROM settings WHERE key = ANY($1)`,
|
||||||
[GROWING_KEYS]
|
[[...GROWING_KEYS, 'growing_smb_password']]
|
||||||
);
|
);
|
||||||
const out = {
|
const out = {
|
||||||
growing_enabled: 'false',
|
|
||||||
growing_path: '/growing',
|
growing_path: '/growing',
|
||||||
growing_smb_url: '',
|
growing_smb_url: '',
|
||||||
|
growing_smb_mount: '',
|
||||||
|
growing_smb_username: '',
|
||||||
|
growing_smb_vers: '3.0',
|
||||||
growing_promote_after_seconds: '8',
|
growing_promote_after_seconds: '8',
|
||||||
|
growing_smb_password_exists: false,
|
||||||
};
|
};
|
||||||
for (const { key, value } of result.rows) out[key] = value;
|
for (const { key, value } of result.rows) {
|
||||||
|
if (key === 'growing_smb_password') {
|
||||||
|
out.growing_smb_password_exists = !!(value && value.length);
|
||||||
|
} else {
|
||||||
|
out[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
res.json(out);
|
res.json(out);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
next(err);
|
next(err);
|
||||||
|
|
@ -290,6 +314,19 @@ router.put('/growing', async (req, res, next) => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// SMB password is write-only. A non-empty value sets/replaces it. To remove
|
||||||
|
// it, send growing_smb_password_clear:true. A blank/omitted password field
|
||||||
|
// leaves the stored value untouched (so operators don't retype it on every
|
||||||
|
// save).
|
||||||
|
if (req.body.growing_smb_password_clear === true) {
|
||||||
|
await pool.query(`DELETE FROM settings WHERE key = 'growing_smb_password'`);
|
||||||
|
} else if (typeof req.body.growing_smb_password === 'string' && req.body.growing_smb_password.length > 0) {
|
||||||
|
await pool.query(
|
||||||
|
`INSERT INTO settings (key, value, updated_at) VALUES ('growing_smb_password', $1, NOW())
|
||||||
|
ON CONFLICT (key) DO UPDATE SET value = $1, updated_at = NOW()`,
|
||||||
|
[req.body.growing_smb_password]
|
||||||
|
);
|
||||||
|
}
|
||||||
res.json({ message: 'Growing-files settings saved' });
|
res.json({ message: 'Growing-files settings saved' });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
next(err);
|
next(err);
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,12 @@ const exec = promisify(execCb);
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// Defaults mirrored from settings.js so the overview never returns nulls.
|
// Defaults mirrored from settings.js so the overview never returns nulls.
|
||||||
|
// Growing-file mode is now per-recorder; "enabled" here means the shared SMB
|
||||||
|
// landing zone is CONFIGURED (a mount source is set), not a global on/off.
|
||||||
const GROWING_DEFAULTS = {
|
const GROWING_DEFAULTS = {
|
||||||
growing_enabled: 'false',
|
|
||||||
growing_path: '/growing',
|
growing_path: '/growing',
|
||||||
growing_smb_url: '',
|
growing_smb_url: '',
|
||||||
|
growing_smb_mount: '',
|
||||||
growing_promote_after_seconds: '8',
|
growing_promote_after_seconds: '8',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -100,7 +102,9 @@ router.get('/overview', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
// Growing files — merge defaults with whatever's in `settings`.
|
// Growing files — merge defaults with whatever's in `settings`.
|
||||||
const growingRaw = { ...GROWING_DEFAULTS, ...(await readSettings(Object.keys(GROWING_DEFAULTS))) };
|
const growingRaw = { ...GROWING_DEFAULTS, ...(await readSettings(Object.keys(GROWING_DEFAULTS))) };
|
||||||
const growingEnabled = growingRaw.growing_enabled === 'true' || growingRaw.growing_enabled === true;
|
// "enabled" now means the shared SMB landing zone is configured (a mount
|
||||||
|
// source is set). Per-recorder toggles decide which recorders actually use it.
|
||||||
|
const growingEnabled = !!(growingRaw.growing_smb_mount && growingRaw.growing_smb_mount.trim());
|
||||||
const containerPath = growingRaw.growing_path || '/growing';
|
const containerPath = growingRaw.growing_path || '/growing';
|
||||||
const mount = await probeGrowingPath(containerPath);
|
const mount = await probeGrowingPath(containerPath);
|
||||||
|
|
||||||
|
|
@ -117,6 +121,7 @@ router.get('/overview', async (req, res, next) => {
|
||||||
// existing deploy uses this symlink — surface it for operator context.
|
// existing deploy uses this symlink — surface it for operator context.
|
||||||
host_path: '/mnt/NVME/MAM/wild-dragon-growing',
|
host_path: '/mnt/NVME/MAM/wild-dragon-growing',
|
||||||
smb_url: growingRaw.growing_smb_url || '',
|
smb_url: growingRaw.growing_smb_url || '',
|
||||||
|
smb_mount: growingRaw.growing_smb_mount || '',
|
||||||
promote_after_seconds: parseInt(growingRaw.growing_promote_after_seconds, 10) || 8,
|
promote_after_seconds: parseInt(growingRaw.growing_promote_after_seconds, 10) || 8,
|
||||||
exists: mount.exists,
|
exists: mount.exists,
|
||||||
writable: mount.writable,
|
writable: mount.writable,
|
||||||
|
|
|
||||||
|
|
@ -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,6 +9,8 @@
|
||||||
|
|
||||||
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 { 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);
|
||||||
const SELF_URL = process.env.MAM_API_SELF_URL || `http://127.0.0.1:${process.env.PORT || 3000}`;
|
const SELF_URL = process.env.MAM_API_SELF_URL || `http://127.0.0.1:${process.env.PORT || 3000}`;
|
||||||
|
|
@ -19,7 +21,10 @@ let _interval = null;
|
||||||
async function callSelf(path, method = 'POST') {
|
async function callSelf(path, method = 'POST') {
|
||||||
const res = await fetch(`${SELF_URL}${path}`, {
|
const res = await fetch(`${SELF_URL}${path}`, {
|
||||||
method,
|
method,
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'x-internal-token': INTERNAL_TOKEN,
|
||||||
|
},
|
||||||
signal: AbortSignal.timeout(30000),
|
signal: AbortSignal.timeout(30000),
|
||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
|
@ -29,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]);
|
||||||
|
|
@ -52,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()
|
||||||
|
|
@ -92,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()
|
||||||
|
|
@ -115,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()
|
||||||
|
|
@ -126,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
|
||||||
|
|
@ -142,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
|
||||||
|
|
@ -161,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')
|
||||||
|
|
@ -175,6 +162,8 @@ async function tick() {
|
||||||
for (const row of ampps.rows) {
|
for (const row of ampps.rows) {
|
||||||
await syncToAmpp(row.id, row.project_id, row.bin_id);
|
await syncToAmpp(row.id, row.project_id, row.bin_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await playoutHealthTick(client);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[scheduler] tick error:', err);
|
console.error('[scheduler] tick error:', err);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -201,11 +190,142 @@ async function enqueueNextOccurrence(schedule, client) {
|
||||||
console.log(`[scheduler] queued next "${schedule.name}" → ${start.toISOString()}`);
|
console.log(`[scheduler] queued next "${schedule.name}" → ${start.toISOString()}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Playout channel health + failover ────────────────────────────────────────
|
||||||
|
// Tick step 6. Reuses the same advisory lock so only one replica probes the
|
||||||
|
// sidecars; multi-replica pings would just waste cycles. A missed probe is
|
||||||
|
// counted via last_heartbeat_at age: > 3 * TICK_INTERVAL means 3 consecutive
|
||||||
|
// misses.
|
||||||
|
// Persist the as-run compliance log for one channel from a sidecar /status
|
||||||
|
// payload. The sidecar reports the currently on-air item via currentItemId /
|
||||||
|
// currentClip / currentItemStartedAt (playout-manager.getStatus). We keep at
|
||||||
|
// most one "open" row (ended_at IS NULL) per channel: when the on-air item
|
||||||
|
// changes (or playout stops) we close the open row — stamping ended_at and a
|
||||||
|
// computed duration_s — and, if a new clip is on air, open a fresh row.
|
||||||
|
//
|
||||||
|
// playout_as_run columns (migration 029): id, channel_id, asset_id, item_id,
|
||||||
|
// clip_name, started_at, ended_at, duration_s, result.
|
||||||
|
async function writeAsRun(client, channelId, engine) {
|
||||||
|
const currentItemId = engine && engine.currentItemId ? engine.currentItemId : null;
|
||||||
|
|
||||||
|
// The currently-open as-run row for this channel, if any.
|
||||||
|
const { rows: openRows } = await client.query(
|
||||||
|
`SELECT id, item_id, started_at FROM playout_as_run
|
||||||
|
WHERE channel_id = $1 AND ended_at IS NULL
|
||||||
|
ORDER BY started_at DESC LIMIT 1`,
|
||||||
|
[channelId]
|
||||||
|
);
|
||||||
|
const open = openRows[0] || null;
|
||||||
|
|
||||||
|
// Same clip still on air → nothing to do.
|
||||||
|
if (open && currentItemId && open.item_id === currentItemId) return;
|
||||||
|
// Nothing on air and nothing open → nothing to do.
|
||||||
|
if (!open && !currentItemId) return;
|
||||||
|
|
||||||
|
// Close the previous open row (clip changed, or playout stopped).
|
||||||
|
if (open) {
|
||||||
|
await client.query(
|
||||||
|
`UPDATE playout_as_run
|
||||||
|
SET ended_at = NOW(),
|
||||||
|
duration_s = EXTRACT(EPOCH FROM (NOW() - started_at))
|
||||||
|
WHERE id = $1`,
|
||||||
|
[open.id]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open a new row for the clip now on air. Resolve the item's asset_id so the
|
||||||
|
// compliance log links back to the source asset even after the playlist item
|
||||||
|
// is later deleted.
|
||||||
|
if (currentItemId) {
|
||||||
|
let assetId = null;
|
||||||
|
try {
|
||||||
|
const { rows } = await client.query(
|
||||||
|
'SELECT asset_id FROM playout_items WHERE id = $1', [currentItemId]
|
||||||
|
);
|
||||||
|
if (rows.length > 0) assetId = rows[0].asset_id;
|
||||||
|
} catch (_) { /* item may have been deleted; log without asset link */ }
|
||||||
|
|
||||||
|
await client.query(
|
||||||
|
`INSERT INTO playout_as_run
|
||||||
|
(channel_id, asset_id, item_id, clip_name, started_at, result)
|
||||||
|
VALUES ($1, $2, $3, $4, COALESCE($5::timestamptz, NOW()), 'played')`,
|
||||||
|
[channelId, assetId, currentItemId, engine.currentClip || null,
|
||||||
|
engine.currentItemStartedAt || null]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function playoutHealthTick(client) {
|
||||||
|
let channels;
|
||||||
|
try {
|
||||||
|
({ rows: channels } = await client.query(
|
||||||
|
`SELECT id, output_type, container_meta, node_id, last_heartbeat_at, updated_at, restart_count
|
||||||
|
FROM playout_channels WHERE status = 'running'`
|
||||||
|
));
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code === '42P01') return;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TIMEOUT_MS = TICK_INTERVAL_MS * 3 + 5000;
|
||||||
|
for (const ch of channels) {
|
||||||
|
const sidecarUrl =
|
||||||
|
ch.container_meta && ch.container_meta.sidecar_url
|
||||||
|
? ch.container_meta.sidecar_url
|
||||||
|
: `http://playout-${ch.id}:3002`;
|
||||||
|
try {
|
||||||
|
const r = await fetch(`${sidecarUrl}/status`, { signal: AbortSignal.timeout(5000) });
|
||||||
|
if (!r.ok) throw new Error(`status HTTP ${r.status}`);
|
||||||
|
await client.query(
|
||||||
|
'UPDATE playout_channels SET last_heartbeat_at = NOW() WHERE id = $1', [ch.id]
|
||||||
|
);
|
||||||
|
// As-run compliance log: the sidecar only tracks the on-air clip locally
|
||||||
|
// (playout-manager._reportAsRunStart). On every successful status poll we
|
||||||
|
// detect a clip change here and persist it to playout_as_run — close the
|
||||||
|
// previous open row and open a new one. Failures are swallowed so a logging
|
||||||
|
// hiccup never knocks a healthy channel into failover.
|
||||||
|
try {
|
||||||
|
const engine = await r.json().catch(() => null);
|
||||||
|
if (engine) await writeAsRun(client, ch.id, engine);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`[scheduler] as-run write failed for ${ch.id}: ${e.message}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// When last_heartbeat_at is NULL (channel just spawned), fall back to
|
||||||
|
// updated_at (set to NOW() by spawnChannelSidecar). This prevents a
|
||||||
|
// brand-new channel from being failed over on the very first tick because
|
||||||
|
// epoch-0 age always exceeds TIMEOUT_MS.
|
||||||
|
const baseline = ch.last_heartbeat_at || ch.updated_at;
|
||||||
|
const lastSeen = baseline ? new Date(baseline).getTime() : Date.now();
|
||||||
|
const ageMs = Date.now() - lastSeen;
|
||||||
|
if (ageMs < TIMEOUT_MS) continue;
|
||||||
|
|
||||||
|
if (ch.output_type === 'decklink') {
|
||||||
|
await client.query(
|
||||||
|
"UPDATE playout_channels SET status = 'error', error_message = $1 WHERE id = $2",
|
||||||
|
[`sidecar unreachable (${err.message}); decklink channels require manual recovery`, ch.id]
|
||||||
|
);
|
||||||
|
console.error(`[scheduler] decklink channel ${ch.id} unreachable — alert-only, no auto-failover`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn(`[scheduler] failover: channel ${ch.id} unreachable (${err.message}), restart #${ch.restart_count + 1}`);
|
||||||
|
try {
|
||||||
|
const res = await restartChannel(ch.id);
|
||||||
|
if (res.restarted) {
|
||||||
|
console.log(`[scheduler] failover: channel ${ch.id} re-placed on node ${res.new_node_id}`);
|
||||||
|
} else {
|
||||||
|
console.error(`[scheduler] failover: channel ${ch.id} restart skipped — ${res.reason}`);
|
||||||
|
}
|
||||||
|
} catch (err2) {
|
||||||
|
console.error(`[scheduler] failover error for ${ch.id}: ${err2.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,16 @@ const NODE_ROLE = process.env.NODE_ROLE || 'worker';
|
||||||
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'];
|
||||||
|
|
||||||
// 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),
|
||||||
|
|
@ -227,6 +236,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 => {
|
||||||
|
|
@ -572,6 +722,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);
|
||||||
|
|
||||||
|
|
|
||||||
102
services/playout/Dockerfile
Normal file
102
services/playout/Dockerfile
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
# Wild Dragon Playout sidecar — CasparCG Server + Node AMCP control shim.
|
||||||
|
#
|
||||||
|
# CasparCG's mixer needs an OpenGL context. On a node with a real GPU we'd pass
|
||||||
|
# the device + driver through; for the headless / no-GPU case we run a virtual
|
||||||
|
# framebuffer (Xvfb) so the GL context initialises. The container is launched
|
||||||
|
# --privileged by mam-api (same as capture) so DeckLink / NDI hardware is
|
||||||
|
# reachable when present.
|
||||||
|
#
|
||||||
|
# CasparCG 2.4.x no longer ships a self-contained Linux tarball — the GitHub
|
||||||
|
# release provides either Ubuntu .deb packages or an "ubuntu22" zip that bundles
|
||||||
|
# the binary + its .so files under bin/ and lib/. We use the zip on an
|
||||||
|
# ubuntu:22.04 base so the bundled libs match the host glibc/abi, then install
|
||||||
|
# Node 20 from NodeSource on top.
|
||||||
|
#
|
||||||
|
# NDI + DeckLink SDKs are NOT redistributable. They are fetched at build time
|
||||||
|
# from a URL supplied as a build arg (mirror it into your own artifact store);
|
||||||
|
# the build still succeeds without it (NDI/DeckLink consumers simply won't be
|
||||||
|
# available — SRT/RTMP/test output still work).
|
||||||
|
|
||||||
|
FROM ubuntu:22.04
|
||||||
|
|
||||||
|
ARG CASPAR_VERSION=2.4.0-stable
|
||||||
|
ARG CASPAR_URL=https://github.com/CasparCG/server/releases/download/v2.4.0-stable/casparcg-server-v2.4.0-stable-ubuntu22.zip
|
||||||
|
ARG NDI_SDK_URL=
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
# CasparCG 2.4 runtime deps + Xvfb for headless GL + CEF (HTML producer) deps +
|
||||||
|
# Node 20 (NodeSource).
|
||||||
|
#
|
||||||
|
# NOTE: we deliberately do NOT `apt-get install ffmpeg`. That package drags in
|
||||||
|
# ~80 transitive shared libraries (libav*, libx264, libdrm, libva, ...) that
|
||||||
|
# perturb CasparCG 2.4.0's runtime linking and make its headless startup abort
|
||||||
|
# with SIGABRT (exit 134) on nearly every launch. A self-contained STATIC
|
||||||
|
# ffmpeg binary (installed below) gives us the standalone CLI the preview
|
||||||
|
# re-muxer needs with ZERO new shared libs, keeping CasparCG's environment
|
||||||
|
# identical to the known-good image.
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
ca-certificates curl unzip tar xz-utils gnupg \
|
||||||
|
xvfb libgl1-mesa-dri libglu1-mesa fonts-dejavu-core \
|
||||||
|
libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 \
|
||||||
|
libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 \
|
||||||
|
libgbm1 libpango-1.0-0 libcairo2 libasound2 libatspi2.0-0 \
|
||||||
|
&& mkdir -p /etc/apt/keyrings \
|
||||||
|
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \
|
||||||
|
| gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
|
||||||
|
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" \
|
||||||
|
> /etc/apt/sources.list.d/nodesource.list \
|
||||||
|
&& apt-get update && apt-get install -y --no-install-recommends nodejs \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# ── Standalone STATIC ffmpeg CLI (for the HLS preview re-muxer) ───────────────
|
||||||
|
# john van sickle's static build is fully self-contained (no shared-lib deps),
|
||||||
|
# so it can't perturb CasparCG's runtime linking. Override FFMPEG_URL to mirror
|
||||||
|
# this into your own artifact store if upstream availability is a concern.
|
||||||
|
ARG FFMPEG_URL=https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz
|
||||||
|
RUN set -eux; \
|
||||||
|
curl -fsSL "$FFMPEG_URL" -o /tmp/ffmpeg.tar.xz; \
|
||||||
|
mkdir -p /tmp/ffmpeg; \
|
||||||
|
tar xJf /tmp/ffmpeg.tar.xz -C /tmp/ffmpeg --strip-components=1; \
|
||||||
|
cp /tmp/ffmpeg/ffmpeg /tmp/ffmpeg/ffprobe /usr/local/bin/; \
|
||||||
|
chmod +x /usr/local/bin/ffmpeg /usr/local/bin/ffprobe; \
|
||||||
|
rm -rf /tmp/ffmpeg /tmp/ffmpeg.tar.xz; \
|
||||||
|
/usr/local/bin/ffmpeg -version | head -1
|
||||||
|
|
||||||
|
# ── CasparCG Server (ubuntu22 zip bundle) ────────────────────────────────────
|
||||||
|
# The zip extracts to /opt/casparcg_server with the binary at bin/casparcg and
|
||||||
|
# its bundled .so files under lib/ (added to LD_LIBRARY_PATH by entrypoint.sh).
|
||||||
|
# Symlink to /opt/casparcg so the config/entrypoint paths stay stable.
|
||||||
|
WORKDIR /tmp/caspar
|
||||||
|
RUN set -eux; \
|
||||||
|
curl -fsSL "$CASPAR_URL" -o caspar.zip; \
|
||||||
|
unzip -q caspar.zip -d /opt; \
|
||||||
|
chmod +x /opt/casparcg_server/bin/casparcg /opt/casparcg_server/scanner 2>/dev/null || true; \
|
||||||
|
ls /opt/casparcg_server/; \
|
||||||
|
test -x /opt/casparcg_server/bin/casparcg; \
|
||||||
|
ln -sfn /opt/casparcg_server /opt/casparcg; \
|
||||||
|
echo "caspar binary: /opt/casparcg_server/bin/casparcg"; \
|
||||||
|
cd /; rm -rf /tmp/caspar
|
||||||
|
|
||||||
|
RUN if [ -n "$NDI_SDK_URL" ]; then \
|
||||||
|
mkdir -p /opt/ndi-lib && \
|
||||||
|
curl -fsSL "$NDI_SDK_URL" -o /tmp/ndi.tar.gz && \
|
||||||
|
tar xzf /tmp/ndi.tar.gz -C /tmp && \
|
||||||
|
find /tmp -name 'libndi*.so*' -exec cp -a {} /opt/ndi-lib/ \; && \
|
||||||
|
rm -f /tmp/ndi.tar.gz && ldconfig /opt/ndi-lib || true; \
|
||||||
|
fi
|
||||||
|
ENV NDI_RUNTIME_DIR_V6=/opt/ndi-lib
|
||||||
|
|
||||||
|
RUN mkdir -p /media
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install --omit=dev
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
COPY casparcg.config /opt/casparcg/casparcg.config
|
||||||
|
COPY entrypoint.sh /entrypoint.sh
|
||||||
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|
||||||
|
EXPOSE 3002 5250
|
||||||
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
22
services/playout/casparcg.config
Normal file
22
services/playout/casparcg.config
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<configuration>
|
||||||
|
<paths>
|
||||||
|
<media-path>/media/</media-path>
|
||||||
|
<log-path>/media/casparcg/log/</log-path>
|
||||||
|
<data-path>/media/casparcg/data/</data-path>
|
||||||
|
<template-path>/media/templates/</template-path>
|
||||||
|
</paths>
|
||||||
|
<channels>
|
||||||
|
<channel>
|
||||||
|
<video-mode>1080i5994</video-mode>
|
||||||
|
<consumers>
|
||||||
|
</consumers>
|
||||||
|
</channel>
|
||||||
|
</channels>
|
||||||
|
<controllers>
|
||||||
|
<tcp>
|
||||||
|
<port>5250</port>
|
||||||
|
<protocol>AMCP</protocol>
|
||||||
|
</tcp>
|
||||||
|
</controllers>
|
||||||
|
</configuration>
|
||||||
57
services/playout/entrypoint.sh
Normal file
57
services/playout/entrypoint.sh
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [ -z "${DISPLAY:-}" ]; then
|
||||||
|
echo "[entrypoint] starting Xvfb on :99"
|
||||||
|
Xvfb :99 -screen 0 1920x1080x24 -nolisten tcp &
|
||||||
|
export DISPLAY=:99
|
||||||
|
for i in $(seq 1 20); do
|
||||||
|
[ -e /tmp/.X11-unix/X99 ] && break
|
||||||
|
sleep 0.25
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure the HLS preview directory exists before the re-mux ffmpeg writes to it
|
||||||
|
# (mam-api serves /live/<channel_id>/* from the shared media volume).
|
||||||
|
if [ -n "${CHANNEL_ID:-}" ]; then
|
||||||
|
mkdir -p "/media/live/${CHANNEL_ID}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p /media/casparcg/log /media/casparcg/data /media/templates
|
||||||
|
|
||||||
|
# CEF (HTML producer) initialises an NSS database at /root/.pki/nssdb and
|
||||||
|
# Chrome caches under HOME. Pre-create writable dirs so CEF doesn't SIGABRT
|
||||||
|
# ~30s into the run when it first lazily inits.
|
||||||
|
mkdir -p /root/.pki/nssdb /root/.cache /tmp/cef-cache
|
||||||
|
chmod 700 /root/.pki/nssdb
|
||||||
|
export HOME=/root
|
||||||
|
|
||||||
|
# 2.4.x zip bundles its own .so files under lib/ — add to LD_LIBRARY_PATH.
|
||||||
|
export LD_LIBRARY_PATH="/opt/casparcg/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
|
||||||
|
|
||||||
|
cd /opt/casparcg
|
||||||
|
CASPAR_CFG=/opt/casparcg/casparcg.config
|
||||||
|
# 2.4.x: binary at bin/casparcg. 2.5.x: symlinked to casparcg at root.
|
||||||
|
if [ -x "./bin/casparcg" ]; then CASPAR_BIN="./bin/casparcg";
|
||||||
|
elif [ -x "./casparcg" ]; then CASPAR_BIN="./casparcg";
|
||||||
|
elif [ -x "./CasparCG Server" ]; then CASPAR_BIN="./CasparCG Server";
|
||||||
|
elif command -v casparcg >/dev/null; then CASPAR_BIN="casparcg";
|
||||||
|
else echo "[entrypoint] ERROR: casparcg binary not found"; exit 1; fi
|
||||||
|
echo "[entrypoint] launching CasparCG: $CASPAR_BIN $CASPAR_CFG"
|
||||||
|
"$CASPAR_BIN" "$CASPAR_CFG" &
|
||||||
|
CASPAR_PID=$!
|
||||||
|
|
||||||
|
term() {
|
||||||
|
echo "[entrypoint] terminating CasparCG ($CASPAR_PID)"
|
||||||
|
kill -TERM "$CASPAR_PID" 2>/dev/null || true
|
||||||
|
wait "$CASPAR_PID" 2>/dev/null || true
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
trap term SIGTERM SIGINT
|
||||||
|
|
||||||
|
cd /app
|
||||||
|
node src/index.js &
|
||||||
|
NODE_PID=$!
|
||||||
|
|
||||||
|
wait -n "$CASPAR_PID" "$NODE_PID"
|
||||||
|
term
|
||||||
18
services/playout/package.json
Normal file
18
services/playout/package.json
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"name": "wild-dragon-playout",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Wild Dragon MAM playout sidecar — wraps a CasparCG Server instance and drives it over AMCP for master-control playout (SDI / NDI / SRT / RTMP).",
|
||||||
|
"type": "module",
|
||||||
|
"main": "src/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node src/index.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^4.18.0",
|
||||||
|
"cors": "^2.8.0",
|
||||||
|
"dotenv": "^16.4.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
182
services/playout/src/amcp.js
Normal file
182
services/playout/src/amcp.js
Normal file
|
|
@ -0,0 +1,182 @@
|
||||||
|
import net from 'node:net';
|
||||||
|
|
||||||
|
// Minimal AMCP (Advanced Media Control Protocol) client for CasparCG.
|
||||||
|
//
|
||||||
|
// AMCP is a line-based TCP protocol: each command is a single CRLF-terminated
|
||||||
|
// line, and the server replies with a status line ("201 PLAY OK\r\n") optionally
|
||||||
|
// followed by data lines. We keep one persistent socket per CasparCG instance
|
||||||
|
// and serialize commands through a FIFO queue — CasparCG processes one command
|
||||||
|
// at a time per connection, so interleaving replies would otherwise be
|
||||||
|
// ambiguous.
|
||||||
|
//
|
||||||
|
// We only implement the subset the playout sidecar needs (PLAY / LOADBG / STOP /
|
||||||
|
// CLEAR / INFO / ADD / REMOVE). Responses are returned raw; callers parse the
|
||||||
|
// status code where they care.
|
||||||
|
|
||||||
|
const CRLF = '\r\n';
|
||||||
|
|
||||||
|
export class AmcpClient {
|
||||||
|
constructor({ host = '127.0.0.1', port = 5250 } = {}) {
|
||||||
|
this.host = host;
|
||||||
|
this.port = port;
|
||||||
|
this.socket = null;
|
||||||
|
this.connected = false;
|
||||||
|
this._buffer = '';
|
||||||
|
this._queue = []; // pending { command, resolve, reject, timer }
|
||||||
|
this._active = null; // command currently awaiting a reply
|
||||||
|
this._reconnectTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
if (this.socket) return;
|
||||||
|
const socket = net.createConnection({ host: this.host, port: this.port });
|
||||||
|
socket.setEncoding('utf8');
|
||||||
|
socket.setKeepAlive(true, 10000);
|
||||||
|
|
||||||
|
socket.on('connect', () => {
|
||||||
|
this.connected = true;
|
||||||
|
console.log(`[amcp] connected to ${this.host}:${this.port}`);
|
||||||
|
});
|
||||||
|
socket.on('data', (chunk) => this._onData(chunk));
|
||||||
|
socket.on('error', (err) => {
|
||||||
|
console.error(`[amcp] socket error: ${err.message}`);
|
||||||
|
});
|
||||||
|
socket.on('close', () => {
|
||||||
|
this.connected = false;
|
||||||
|
this.socket = null;
|
||||||
|
// Fail any in-flight + queued commands so callers don't hang.
|
||||||
|
const pending = this._active ? [this._active, ...this._queue] : [...this._queue];
|
||||||
|
this._active = null;
|
||||||
|
this._queue = [];
|
||||||
|
for (const p of pending) {
|
||||||
|
clearTimeout(p.timer);
|
||||||
|
p.reject(new Error('AMCP connection closed'));
|
||||||
|
}
|
||||||
|
this._scheduleReconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket = socket;
|
||||||
|
}
|
||||||
|
|
||||||
|
_scheduleReconnect() {
|
||||||
|
if (this._reconnectTimer) return;
|
||||||
|
this._reconnectTimer = setTimeout(() => {
|
||||||
|
this._reconnectTimer = null;
|
||||||
|
console.log('[amcp] reconnecting...');
|
||||||
|
this.connect();
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait until the socket is usable, up to timeoutMs.
|
||||||
|
async waitReady(timeoutMs = 30000) {
|
||||||
|
const deadline = Date.now() + timeoutMs;
|
||||||
|
while (Date.now() < deadline) {
|
||||||
|
if (this.connected) return true;
|
||||||
|
if (!this.socket) this.connect();
|
||||||
|
await new Promise((r) => setTimeout(r, 250));
|
||||||
|
}
|
||||||
|
throw new Error('AMCP not ready within timeout');
|
||||||
|
}
|
||||||
|
|
||||||
|
_onData(chunk) {
|
||||||
|
this._buffer += chunk;
|
||||||
|
// A CasparCG reply is a status line, optionally followed by data lines.
|
||||||
|
// The simplest robust framing: a command's reply is complete when we see a
|
||||||
|
// status line AND (for 2-line "200" multi-line replies) the terminating
|
||||||
|
// blank line. For our command subset, single-status-line replies dominate;
|
||||||
|
// we treat a reply as complete at each newline and let the active command
|
||||||
|
// decide whether it has enough. To keep this correct for INFO (multi-line),
|
||||||
|
// we accumulate until the buffer ends with a known terminator.
|
||||||
|
if (!this._active) {
|
||||||
|
// Unsolicited data (e.g. connection banner) — discard.
|
||||||
|
this._buffer = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// CasparCG ends multi-line replies with CRLF on an empty line. Single-line
|
||||||
|
// replies (201/202/4xx/5xx) end with a single CRLF. Resolve when we have at
|
||||||
|
// least one complete line; for "200 ... OK" (list follows) wait for the
|
||||||
|
// blank-line terminator.
|
||||||
|
const firstLineEnd = this._buffer.indexOf(CRLF);
|
||||||
|
if (firstLineEnd === -1) return;
|
||||||
|
const statusLine = this._buffer.slice(0, firstLineEnd);
|
||||||
|
const code = parseInt(statusLine, 10);
|
||||||
|
|
||||||
|
if (code === 200) {
|
||||||
|
// Multi-line: data lines until an empty line.
|
||||||
|
const term = this._buffer.indexOf(CRLF + CRLF);
|
||||||
|
if (term === -1) return; // wait for more
|
||||||
|
const full = this._buffer.slice(0, term);
|
||||||
|
this._buffer = this._buffer.slice(term + 4);
|
||||||
|
this._finishActive(null, full);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code === 201 || code === 202) {
|
||||||
|
// 201: one data line follows the status line. 202: status only.
|
||||||
|
if (code === 201) {
|
||||||
|
const secondLineEnd = this._buffer.indexOf(CRLF, firstLineEnd + 2);
|
||||||
|
if (secondLineEnd === -1) return;
|
||||||
|
const full = this._buffer.slice(0, secondLineEnd);
|
||||||
|
this._buffer = this._buffer.slice(secondLineEnd + 2);
|
||||||
|
this._finishActive(null, full);
|
||||||
|
} else {
|
||||||
|
const full = this._buffer.slice(0, firstLineEnd);
|
||||||
|
this._buffer = this._buffer.slice(firstLineEnd + 2);
|
||||||
|
this._finishActive(null, full);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4xx / 5xx error, or any other single-line status.
|
||||||
|
const full = this._buffer.slice(0, firstLineEnd);
|
||||||
|
this._buffer = this._buffer.slice(firstLineEnd + 2);
|
||||||
|
if (code >= 400) this._finishActive(new Error(`AMCP error: ${full}`), full);
|
||||||
|
else this._finishActive(null, full);
|
||||||
|
}
|
||||||
|
|
||||||
|
_finishActive(err, data) {
|
||||||
|
const active = this._active;
|
||||||
|
this._active = null;
|
||||||
|
if (active) {
|
||||||
|
clearTimeout(active.timer);
|
||||||
|
if (err) active.reject(err);
|
||||||
|
else active.resolve(data);
|
||||||
|
}
|
||||||
|
this._pump();
|
||||||
|
}
|
||||||
|
|
||||||
|
_pump() {
|
||||||
|
if (this._active || this._queue.length === 0) return;
|
||||||
|
const next = this._queue.shift();
|
||||||
|
this._active = next;
|
||||||
|
try {
|
||||||
|
this.socket.write(next.command + CRLF);
|
||||||
|
} catch (err) {
|
||||||
|
this._active = null;
|
||||||
|
clearTimeout(next.timer);
|
||||||
|
next.reject(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a single AMCP command and resolve with the raw reply string.
|
||||||
|
send(command, { timeoutMs = 15000 } = {}) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const entry = { command, resolve, reject, timer: null };
|
||||||
|
entry.timer = setTimeout(() => {
|
||||||
|
// Drop from queue if still pending; if active, detach so the next
|
||||||
|
// reply doesn't get misrouted.
|
||||||
|
if (this._active === entry) this._active = null;
|
||||||
|
else this._queue = this._queue.filter((e) => e !== entry);
|
||||||
|
reject(new Error(`AMCP command timed out: ${command}`));
|
||||||
|
}, timeoutMs);
|
||||||
|
this._queue.push(entry);
|
||||||
|
this._pump();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
if (this._reconnectTimer) { clearTimeout(this._reconnectTimer); this._reconnectTimer = null; }
|
||||||
|
if (this.socket) { try { this.socket.destroy(); } catch (_) {} this.socket = null; }
|
||||||
|
this.connected = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
85
services/playout/src/index.js
Normal file
85
services/playout/src/index.js
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
import express from 'express';
|
||||||
|
import cors from 'cors';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
import playoutManager from './playout-manager.js';
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const PORT = process.env.PORT || 3002;
|
||||||
|
|
||||||
|
app.use(cors());
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
app.get('/health', (req, res) => res.json({ status: 'ok' }));
|
||||||
|
|
||||||
|
// Start the channel's output consumer. Body: { outputType, outputConfig, videoFormat }
|
||||||
|
app.post('/channel/start', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const out = await playoutManager.startChannel(req.body || {});
|
||||||
|
res.json(out);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[playout] /channel/start error:', err.message);
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/channel/stop', async (req, res) => {
|
||||||
|
try { res.json(await playoutManager.stopChannel()); }
|
||||||
|
catch (err) { res.status(500).json({ error: err.message }); }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load + start a playlist. Body: { items: [...], loop }
|
||||||
|
app.post('/playlist/load', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { items = [], loop = false } = req.body || {};
|
||||||
|
res.json(await playoutManager.loadPlaylist({ items, loop }));
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[playout] /playlist/load error:', err.message);
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/transport/skip', async (req, res) => { try { res.json(await playoutManager.skip()); } 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.get('/status', (req, res) => res.json(playoutManager.getStatus()));
|
||||||
|
|
||||||
|
// Auto-start: when the sidecar is spawned by mam-api with channel env, bring up
|
||||||
|
// the output consumer immediately so the container is "on air idle" (black/slate)
|
||||||
|
// the moment it boots, mirroring the capture sidecar's bootstrap pattern.
|
||||||
|
async function bootstrap() {
|
||||||
|
const outputType = process.env.OUTPUT_TYPE;
|
||||||
|
if (!outputType) {
|
||||||
|
console.log('[bootstrap] no OUTPUT_TYPE — on-demand sidecar, waiting for /channel/start');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let outputConfig = {};
|
||||||
|
try { outputConfig = JSON.parse(process.env.OUTPUT_CONFIG || '{}'); }
|
||||||
|
catch (err) { console.error('[bootstrap] bad OUTPUT_CONFIG json:', err.message); }
|
||||||
|
const videoFormat = process.env.VIDEO_FORMAT || '1080i5994';
|
||||||
|
try {
|
||||||
|
await playoutManager.startChannel({ outputType, outputConfig, videoFormat });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[bootstrap] channel start failed:', err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = app.listen(PORT, () => {
|
||||||
|
console.log(`Wild Dragon Playout Service listening on port ${PORT}`);
|
||||||
|
// Give CasparCG a moment to come up (started by the container entrypoint).
|
||||||
|
playoutManager.amcp.connect();
|
||||||
|
bootstrap();
|
||||||
|
});
|
||||||
|
|
||||||
|
function shutdown(sig) {
|
||||||
|
console.log(`[playout] ${sig} — shutting down`);
|
||||||
|
playoutManager.stopChannel().catch(() => {}).finally(() => {
|
||||||
|
playoutManager.amcp.close();
|
||||||
|
server.close(() => process.exit(0));
|
||||||
|
setTimeout(() => process.exit(0), 5000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
||||||
|
process.on('SIGINT', () => shutdown('SIGINT'));
|
||||||
458
services/playout/src/playout-manager.js
Normal file
458
services/playout/src/playout-manager.js
Normal file
|
|
@ -0,0 +1,458 @@
|
||||||
|
import { AmcpClient } from './amcp.js';
|
||||||
|
import { spawn } from 'node:child_process';
|
||||||
|
import { mkdirSync, readdirSync, unlinkSync } from 'node:fs';
|
||||||
|
|
||||||
|
// Playout manager — owns one CasparCG channel's lifecycle inside this sidecar.
|
||||||
|
//
|
||||||
|
// One sidecar container == one CasparCG Server == one logical channel (channel
|
||||||
|
// index 1 in CasparCG terms). We add the output consumer (DeckLink / NDI / SRT
|
||||||
|
// / RTMP) at start, then walk a playlist by cueing the next clip on a background
|
||||||
|
// layer (LOADBG ... AUTO) so CasparCG performs a gapless transition at end of
|
||||||
|
// the current clip.
|
||||||
|
//
|
||||||
|
// Media is referenced by a path relative to CasparCG's configured media folder
|
||||||
|
// (/media inside the container). The mam-api stages assets from S3 to that
|
||||||
|
// shared volume and passes the resolved relative path on each item.
|
||||||
|
|
||||||
|
const CHANNEL = 1; // single CasparCG channel per sidecar
|
||||||
|
const FG_LAYER = 10; // foreground (on-air) layer
|
||||||
|
const MEDIA_ROOT = process.env.CASPAR_MEDIA_ROOT || '/media';
|
||||||
|
|
||||||
|
// Channel-id-derived HLS preview path. The mam-api proxies /live/<channel_id>/
|
||||||
|
// to this directory (shared media volume) so the UI's existing HLS player
|
||||||
|
// (capture's /live/<id> plumbing) works for playout monitors with zero new
|
||||||
|
// transport.
|
||||||
|
const CHANNEL_ID = process.env.CHANNEL_ID || '';
|
||||||
|
const HLS_DIR = CHANNEL_ID ? `${MEDIA_ROOT}/live/${CHANNEL_ID}` : '';
|
||||||
|
|
||||||
|
// Loopback UDP port CasparCG's preview STREAM consumer publishes mpegts to, and
|
||||||
|
// the standalone ffmpeg re-muxer reads from. One CasparCG per sidecar, so a
|
||||||
|
// fixed port is fine; allow override for parallel local testing.
|
||||||
|
const PREVIEW_UDP_PORT = parseInt(process.env.PREVIEW_UDP_PORT || '9710', 10);
|
||||||
|
const PREVIEW_UDP_URL = `udp://127.0.0.1:${PREVIEW_UDP_PORT}`;
|
||||||
|
|
||||||
|
// CasparCG SEEK / LENGTH are in frames, not seconds. Capture standard is 59.94;
|
||||||
|
// SD/film modes need their own values. Default 60000/1001 matches both
|
||||||
|
// '1080p5994' and '1080i5994'.
|
||||||
|
function fpsFor(videoFormat) {
|
||||||
|
const f = String(videoFormat || '').toLowerCase();
|
||||||
|
if (f.endsWith('5994')) return 60000 / 1001;
|
||||||
|
if (f.endsWith('p60') || f.endsWith('i60')) return 60;
|
||||||
|
if (f.endsWith('p50') || f.endsWith('i50')) return 50;
|
||||||
|
if (f.endsWith('2997')) return 30000 / 1001;
|
||||||
|
if (f.endsWith('p30')) return 30;
|
||||||
|
if (f.endsWith('p25')) return 25;
|
||||||
|
if (f.endsWith('p24') || f.endsWith('2398')) return 24000 / 1001;
|
||||||
|
return 60000 / 1001; // safe default for the house standard
|
||||||
|
}
|
||||||
|
|
||||||
|
// CasparCG transition syntax fragments keyed by our item.transition value.
|
||||||
|
function transitionArgs(transition, ms, fps) {
|
||||||
|
if (!transition || transition === 'cut' || !ms) return '';
|
||||||
|
const frames = Math.max(1, Math.round((ms / 1000) * fps));
|
||||||
|
if (transition === 'mix') return ` MIX ${frames}`;
|
||||||
|
if (transition === 'wipe') return ` WIPE ${frames}`;
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Turn an absolute /media path (or a relative one) into the token CasparCG
|
||||||
|
// expects: a path relative to MEDIA_ROOT, without extension, forward-slashed.
|
||||||
|
// CasparCG resolves "subdir/clip" against its media folder + probes extensions.
|
||||||
|
function toCasparToken(mediaPath) {
|
||||||
|
let p = String(mediaPath || '');
|
||||||
|
if (p.startsWith(MEDIA_ROOT)) p = p.slice(MEDIA_ROOT.length);
|
||||||
|
p = p.replace(/^\/+/, '');
|
||||||
|
p = p.replace(/\.[^/.]+$/, ''); // strip extension
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PlayoutManager {
|
||||||
|
constructor() {
|
||||||
|
this.amcp = new AmcpClient({
|
||||||
|
host: process.env.CASPAR_HOST || '127.0.0.1',
|
||||||
|
port: parseInt(process.env.CASPAR_PORT || '5250', 10),
|
||||||
|
});
|
||||||
|
this.state = {
|
||||||
|
running: false,
|
||||||
|
outputType: null,
|
||||||
|
outputConfig: null,
|
||||||
|
videoFormat: null,
|
||||||
|
playlist: [], // resolved items in play order
|
||||||
|
currentIndex: -1,
|
||||||
|
loop: false,
|
||||||
|
currentClip: null,
|
||||||
|
startedAt: null,
|
||||||
|
lastError: null,
|
||||||
|
};
|
||||||
|
this._advanceTimer = null;
|
||||||
|
this._hlsProc = null; // standalone ffmpeg re-mux child process
|
||||||
|
this._hlsRestartTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _consumerCommand(outputType, cfg) {
|
||||||
|
// Returns the AMCP ADD argument string for the requested output target.
|
||||||
|
if (outputType === 'decklink') {
|
||||||
|
const dev = cfg.device_index || 1;
|
||||||
|
return `DECKLINK DEVICE ${dev} EMBEDDED_AUDIO`;
|
||||||
|
}
|
||||||
|
if (outputType === 'ndi') {
|
||||||
|
const name = cfg.ndi_name || 'DRAGONFLIGHT';
|
||||||
|
return `NDI NAME "${name}"`;
|
||||||
|
}
|
||||||
|
if (outputType === 'srt' || outputType === 'rtmp') {
|
||||||
|
// CasparCG 2.3 streams via the FFMPEG consumer, invoked with the STREAM
|
||||||
|
// keyword (FILE/STREAM are interchangeable aliases for it; the bare word
|
||||||
|
// "FFMPEG" is the PRODUCER and is NOT a valid consumer keyword). Args must
|
||||||
|
// use ffmpeg's -param:stream form (-codec:v, not -vcodec) or CasparCG
|
||||||
|
// rejects them. The channel feeds the consumer as RGBA, so a
|
||||||
|
// format=yuv420p filter is required before libx264.
|
||||||
|
const url = cfg.url || '';
|
||||||
|
if (outputType === 'srt') {
|
||||||
|
const latency = cfg.latency || 200;
|
||||||
|
const full = url.includes('latency=') ? url : `${url}${url.includes('?') ? '&' : '?'}latency=${latency}`;
|
||||||
|
return `STREAM "${full}" -format mpegts -codec:v libx264 -preset:v veryfast -tune:v zerolatency -b:v 6M -codec:a aac -b:a 192k -filter:v format=yuv420p`;
|
||||||
|
}
|
||||||
|
const target = cfg.key ? `${url}/${cfg.key}` : url;
|
||||||
|
return `STREAM "${target}" -format flv -codec:v libx264 -preset:v veryfast -tune:v zerolatency -b:v 6M -codec:a aac -b:a 192k -filter:v format=yuv420p`;
|
||||||
|
}
|
||||||
|
throw new Error(`Unknown output_type: ${outputType}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the channel: bring up CasparCG's primary output consumer for the
|
||||||
|
// target, plus a second FFMPEG consumer writing HLS for the UI preview
|
||||||
|
// monitor (~4-6s lag, reuses capture's /live/<id> plumbing).
|
||||||
|
//
|
||||||
|
// The primary consumer failure is NON-FATAL. CasparCG can decode and route
|
||||||
|
// media through its pipeline even without an output consumer. This means:
|
||||||
|
// - NDI channels work (load/play/transport) even if libndi.so is absent.
|
||||||
|
// - SRT/RTMP channels work even if the destination URL is unreachable.
|
||||||
|
// - The HLS preview consumer is always attempted independently.
|
||||||
|
//
|
||||||
|
// state.consumerError is set when the primary consumer fails so the mam-api
|
||||||
|
// can surface a warning in the channel status without blocking operation.
|
||||||
|
async startChannel({ outputType, outputConfig = {}, videoFormat = '1080p5994' }) {
|
||||||
|
await this.amcp.waitReady(30000);
|
||||||
|
|
||||||
|
// Set the channel video mode first.
|
||||||
|
try { await this.amcp.send(`SET ${CHANNEL} MODE ${videoFormat}`); }
|
||||||
|
catch (err) { console.warn(`[playout] SET MODE failed (continuing): ${err.message}`); }
|
||||||
|
|
||||||
|
// Primary output consumer — non-fatal.
|
||||||
|
let consumerError = null;
|
||||||
|
try {
|
||||||
|
const consumer = await this._consumerCommand(outputType, outputConfig);
|
||||||
|
await this.amcp.send(`ADD ${CHANNEL} ${consumer}`);
|
||||||
|
} catch (err) {
|
||||||
|
consumerError = err.message;
|
||||||
|
console.warn(`[playout] primary consumer ADD failed (continuing without output): ${err.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// HLS preview consumer — always attempt, independently non-fatal.
|
||||||
|
if (HLS_DIR) {
|
||||||
|
try {
|
||||||
|
await this._addHlsConsumer();
|
||||||
|
console.log(`[playout] HLS preview at ${HLS_DIR}/index.m3u8`);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`[playout] HLS preview consumer failed: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state.running = true;
|
||||||
|
this.state.outputType = outputType;
|
||||||
|
this.state.outputConfig = outputConfig;
|
||||||
|
this.state.videoFormat = videoFormat;
|
||||||
|
this.state.fps = fpsFor(videoFormat);
|
||||||
|
this.state.startedAt = new Date().toISOString();
|
||||||
|
this.state.lastError = consumerError;
|
||||||
|
console.log(`[playout] channel started output=${outputType} mode=${videoFormat} fps=${this.state.fps.toFixed(3)}${consumerError ? ' ⚠ consumer: ' + consumerError : ''}`);
|
||||||
|
return this.getStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
// HLS preview for the web UI confidence monitor.
|
||||||
|
//
|
||||||
|
// ── Why not CasparCG's own HLS (FILE/STREAM ".../index.m3u8") ──────────────
|
||||||
|
// CasparCG's bundled FFMPEG consumer muxes a BROKEN audio track into the HLS:
|
||||||
|
// ffprobe reports `aac, sample_rate=0` and ffmpeg decoding the playlist fails
|
||||||
|
// with "Invalid data ... abuffer: Value inf for parameter 'time_base' ...
|
||||||
|
// time_base 1/0". That corrupt audio prevents BOTH ffmpeg and hls.js from
|
||||||
|
// decoding, so the browser <video> sits at readyState 0 and the preview stays
|
||||||
|
// black. The video track itself is perfectly clean h264. Critically, the
|
||||||
|
// consumer IGNORES every arg that would fix it — `-an`, `-codec:a`, `-g`,
|
||||||
|
// `-r`, `-force_key_frames` are all silently dropped ("Unused option"), so we
|
||||||
|
// CANNOT remove the audio from inside CasparCG.
|
||||||
|
//
|
||||||
|
// ── The fix: STREAM mpegts to UDP loopback, re-mux with a STANDALONE ffmpeg ─
|
||||||
|
// CasparCG outputs a plain mpegts elementary stream to a local UDP port (its
|
||||||
|
// STREAM/mpegts path is fine — the breakage is specific to its HLS muxer). A
|
||||||
|
// Node-spawned standalone ffmpeg (where `-an` actually works) reads that UDP
|
||||||
|
// stream, drops audio, copies the clean h264 video, and writes proper HLS.
|
||||||
|
// `-c:v copy` avoids re-encoding. The program audio is untouched — it rides
|
||||||
|
// the PRIMARY SRT/RTMP/SDI/NDI consumer, which we never modify.
|
||||||
|
async _addHlsConsumer() {
|
||||||
|
// 1) CasparCG → mpegts over UDP loopback. The channel feeds RGBA, so a
|
||||||
|
// format=yuv420p filter is required before libx264.
|
||||||
|
const streamArgs = [
|
||||||
|
`STREAM "${PREVIEW_UDP_URL}?pkt_size=1316"`,
|
||||||
|
'-format mpegts',
|
||||||
|
'-codec:v libx264 -preset:v veryfast -tune:v zerolatency',
|
||||||
|
'-b:v 2M -maxrate 2M -bufsize 4M',
|
||||||
|
'-codec:a aac -b:a 96k',
|
||||||
|
'-filter:v format=yuv420p',
|
||||||
|
].join(' ');
|
||||||
|
await this.amcp.send(`ADD ${CHANNEL} ${streamArgs}`);
|
||||||
|
|
||||||
|
// 2) Standalone ffmpeg re-mux: UDP mpegts → clean video-only HLS.
|
||||||
|
this._startHlsRemux();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spawn (or respawn) the standalone ffmpeg that re-muxes the loopback mpegts
|
||||||
|
// into video-only HLS. Restarts automatically if it dies while the channel is
|
||||||
|
// still running (e.g. brief UDP gap before CasparCG's consumer is up).
|
||||||
|
_startHlsRemux() {
|
||||||
|
if (!HLS_DIR) return;
|
||||||
|
try { mkdirSync(HLS_DIR, { recursive: true }); } catch (_) {}
|
||||||
|
// Purge stale HLS artifacts from any prior session before starting. The
|
||||||
|
// /media volume is a shared host bind, so a previous (or duplicate/failover)
|
||||||
|
// sidecar can leave orphaned index*.ts + an old index.m3u8 behind. ffmpeg's
|
||||||
|
// index%d.ts counter restarts at 0, so those leftovers collide with the new
|
||||||
|
// segment numbering and can briefly corrupt the live playlist hls.js reads
|
||||||
|
// (it sees a frozen / non-monotonic edge → monitor goes black). A clean dir
|
||||||
|
// per session guarantees a coherent live timeline.
|
||||||
|
try {
|
||||||
|
for (const f of readdirSync(HLS_DIR)) {
|
||||||
|
if (/\.ts$/.test(f) || /\.m3u8$/.test(f)) {
|
||||||
|
try { unlinkSync(`${HLS_DIR}/${f}`); } catch (_) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
this._stopHlsRemux();
|
||||||
|
|
||||||
|
const out = `${HLS_DIR}/index.m3u8`;
|
||||||
|
const args = [
|
||||||
|
'-hide_banner', '-loglevel', 'warning',
|
||||||
|
// Read the live mpegts loopback. genpts rebuilds timestamps; the analyze/
|
||||||
|
// probe sizes are kept small so playback starts promptly.
|
||||||
|
'-fflags', '+genpts',
|
||||||
|
'-analyzeduration', '2000000', '-probesize', '2000000',
|
||||||
|
'-i', `${PREVIEW_UDP_URL}?fifo_size=1000000&overrun_nonfatal=1`,
|
||||||
|
// Drop the (broken) audio entirely.
|
||||||
|
'-an',
|
||||||
|
// Re-encode (NOT -c:v copy) to uniform, keyframe-aligned 2s segments with
|
||||||
|
// regenerated CFR timestamps. -c:v copy passed CasparCG's erratic
|
||||||
|
// real-time keyframes straight through, producing segments of 0.6–2.8s
|
||||||
|
// and irregular PTS; hls.js can't build a live timeline from that — it
|
||||||
|
// logs "sliding 0.00 / MISSED", never loads a fragment, and the monitor
|
||||||
|
// stays black even though the stream decodes cleanly server-side. A
|
||||||
|
// standalone ffmpeg honours -force_key_frames, so every GOP (and thus
|
||||||
|
// every HLS segment) is exactly 2.0s.
|
||||||
|
//
|
||||||
|
// This is a CONFIDENCE MONITOR, kept deliberately tiny: 360p / 20fps /
|
||||||
|
// ultrafast. The sidecar has no NVENC, so this is a CPU libx264 encode
|
||||||
|
// running ALONGSIDE CasparCG's mixer + its own STREAM consumer. At 720p30
|
||||||
|
// the re-encode couldn't sustain real time, the UDP input overran, and the
|
||||||
|
// HLS output stalled (playlist froze → monitor black). 360p20 ultrafast is
|
||||||
|
// a fraction of the cost and keeps up comfortably. fps=20 forces CFR;
|
||||||
|
// -g 40 = 2.0s GOP at 20fps.
|
||||||
|
'-vf', 'fps=20,scale=-2:360,format=yuv420p',
|
||||||
|
'-c:v', 'libx264', '-preset', 'ultrafast', '-tune', 'zerolatency',
|
||||||
|
'-b:v', '600k', '-maxrate', '800k', '-bufsize', '1200k',
|
||||||
|
'-g', '40', '-keyint_min', '40', '-sc_threshold', '0',
|
||||||
|
'-force_key_frames', 'expr:gte(t,n_forced*2)',
|
||||||
|
'-f', 'hls',
|
||||||
|
'-hls_time', '2',
|
||||||
|
'-hls_list_size', '8',
|
||||||
|
'-hls_flags', 'delete_segments+append_list+independent_segments',
|
||||||
|
'-hls_segment_filename', `${HLS_DIR}/index%d.ts`,
|
||||||
|
out,
|
||||||
|
];
|
||||||
|
const proc = spawn('ffmpeg', args, { stdio: ['ignore', 'ignore', 'pipe'] });
|
||||||
|
this._hlsProc = proc;
|
||||||
|
proc.stderr.on('data', (d) => {
|
||||||
|
const line = d.toString().trim();
|
||||||
|
if (line) console.warn(`[playout][hls-ffmpeg] ${line}`);
|
||||||
|
});
|
||||||
|
proc.on('exit', (code, signal) => {
|
||||||
|
console.warn(`[playout] HLS re-mux ffmpeg exited code=${code} signal=${signal}`);
|
||||||
|
if (this._hlsProc === proc) this._hlsProc = null;
|
||||||
|
// Auto-respawn while the channel is running (and we didn't kill it).
|
||||||
|
if (this.state.running && signal !== 'SIGTERM' && signal !== 'SIGKILL') {
|
||||||
|
this._hlsRestartTimer = setTimeout(() => {
|
||||||
|
this._hlsRestartTimer = null;
|
||||||
|
if (this.state.running) {
|
||||||
|
console.log('[playout] respawning HLS re-mux ffmpeg');
|
||||||
|
this._startHlsRemux();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
proc.on('error', (err) => {
|
||||||
|
console.warn(`[playout] HLS re-mux ffmpeg spawn error: ${err.message}`);
|
||||||
|
});
|
||||||
|
console.log(`[playout] HLS re-mux ffmpeg started: ${PREVIEW_UDP_URL} -> ${out}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
_stopHlsRemux() {
|
||||||
|
if (this._hlsRestartTimer) {
|
||||||
|
clearTimeout(this._hlsRestartTimer);
|
||||||
|
this._hlsRestartTimer = null;
|
||||||
|
}
|
||||||
|
if (this._hlsProc) {
|
||||||
|
const proc = this._hlsProc;
|
||||||
|
this._hlsProc = null;
|
||||||
|
try { proc.kill('SIGTERM'); } catch (_) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async stopChannel() {
|
||||||
|
this._clearAdvance();
|
||||||
|
this.state.running = false; // set first so the ffmpeg exit handler won't respawn
|
||||||
|
this._stopHlsRemux();
|
||||||
|
try { await this.amcp.send(`STOP ${CHANNEL}-${FG_LAYER}`); } catch (_) {}
|
||||||
|
try { await this.amcp.send(`CLEAR ${CHANNEL}`); } catch (_) {}
|
||||||
|
this.state.running = false;
|
||||||
|
this.state.playlist = [];
|
||||||
|
this.state.currentIndex = -1;
|
||||||
|
this.state.currentClip = null;
|
||||||
|
console.log('[playout] channel stopped');
|
||||||
|
return { stopped: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load a playlist (array of { id, asset_id, media_path, in_point, out_point,
|
||||||
|
// transition, transition_ms, clip_name }) and start playing from index 0.
|
||||||
|
async loadPlaylist({ items = [], loop = false }) {
|
||||||
|
if (!this.state.running) {
|
||||||
|
throw new Error('Channel not started — call /channel/start first');
|
||||||
|
}
|
||||||
|
this.state.playlist = items;
|
||||||
|
this.state.loop = !!loop;
|
||||||
|
this.state.currentIndex = -1;
|
||||||
|
if (items.length === 0) return this.getStatus();
|
||||||
|
await this._playIndex(0);
|
||||||
|
return this.getStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
async _playIndex(index) {
|
||||||
|
const item = this.state.playlist[index];
|
||||||
|
if (!item) return;
|
||||||
|
const fps = this.state.fps || fpsFor(this.state.videoFormat);
|
||||||
|
const token = toCasparToken(item.media_path);
|
||||||
|
const seek = item.in_point ? ` SEEK ${Math.round(item.in_point * fps)}` : '';
|
||||||
|
const length = (item.out_point && item.out_point > (item.in_point || 0))
|
||||||
|
? ` LENGTH ${Math.round((item.out_point - (item.in_point || 0)) * fps)}`
|
||||||
|
: '';
|
||||||
|
const trans = transitionArgs(item.transition, item.transition_ms, fps);
|
||||||
|
|
||||||
|
// PLAY puts the clip on the foreground layer immediately (first clip), with
|
||||||
|
// the configured transition. Subsequent clips are cued via LOADBG ... AUTO
|
||||||
|
// for a gapless hand-off; see _scheduleAdvance.
|
||||||
|
await this.amcp.send(`PLAY ${CHANNEL}-${FG_LAYER} "${token}"${seek}${length}${trans}`);
|
||||||
|
this.state.currentIndex = index;
|
||||||
|
this.state.currentClip = item.clip_name || token;
|
||||||
|
console.log(`[playout] PLAY [${index}] ${token}`);
|
||||||
|
this._reportAsRunStart(item);
|
||||||
|
this._scheduleAdvance(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Effective on-air duration of an item in milliseconds. Prefers an explicit
|
||||||
|
// in/out trim, else the asset's full duration. Returns null when unknown (no
|
||||||
|
// duration metadata + no out_point) so the caller can skip the timer.
|
||||||
|
_itemDurationMs(item) {
|
||||||
|
const inS = item.in_point || 0;
|
||||||
|
if (item.out_point && item.out_point > inS) return (item.out_point - inS) * 1000;
|
||||||
|
if (item.asset_duration_ms != null) return Math.max(0, item.asset_duration_ms - inS * 1000);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CasparCG's LOADBG ... AUTO swaps the cued background clip to foreground when
|
||||||
|
// the current clip ends, giving a gapless visual take. But CasparCG won't cue
|
||||||
|
// clip N+2 on its own and won't move OUR pointer / as-run bookkeeping. So we
|
||||||
|
// also arm a duration-based timer: when the current clip is due to end we
|
||||||
|
// advance currentIndex and cue the following clip. This keeps an arbitrary-
|
||||||
|
// length playlist walking, not just the first two items.
|
||||||
|
_scheduleAdvance(item) {
|
||||||
|
this._clearAdvance();
|
||||||
|
const next = this._nextIndex();
|
||||||
|
if (next === null) return; // end of a non-looping playlist
|
||||||
|
const nextItem = this.state.playlist[next];
|
||||||
|
const nextToken = toCasparToken(nextItem.media_path);
|
||||||
|
const fps = this.state.fps || fpsFor(this.state.videoFormat);
|
||||||
|
const trans = transitionArgs(nextItem.transition, nextItem.transition_ms, fps);
|
||||||
|
// Cue next on background with AUTO so CasparCG performs the gapless take.
|
||||||
|
this.amcp.send(`LOADBG ${CHANNEL}-${FG_LAYER} "${nextToken}" AUTO${trans}`)
|
||||||
|
.catch((err) => console.warn(`[playout] LOADBG failed: ${err.message}`));
|
||||||
|
|
||||||
|
// Arm the pointer-advance timer. Without duration metadata we can't time the
|
||||||
|
// hand-off; leave AUTO to take clip N+1 visually but log a warning since the
|
||||||
|
// pointer (and thus clip N+2 cueing) will stall.
|
||||||
|
const durMs = this._itemDurationMs(item);
|
||||||
|
if (durMs == null) {
|
||||||
|
console.warn(`[playout] no duration for clip [${this.state.currentIndex}] — pointer advance stalled after this clip`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._advanceTimer = setTimeout(() => {
|
||||||
|
this._advanceTimer = null;
|
||||||
|
// The AUTO take already happened in CasparCG; just move our pointer and
|
||||||
|
// cue the clip after next. _playIndex would re-PLAY and double-take, so we
|
||||||
|
// advance state directly and re-arm.
|
||||||
|
this.state.currentIndex = next;
|
||||||
|
this.state.currentClip = nextItem.clip_name || nextToken;
|
||||||
|
console.log(`[playout] advance -> [${next}] ${nextToken}`);
|
||||||
|
this._reportAsRunStart(nextItem);
|
||||||
|
this._scheduleAdvance(nextItem);
|
||||||
|
}, Math.max(250, durMs));
|
||||||
|
}
|
||||||
|
|
||||||
|
_nextIndex() {
|
||||||
|
const n = this.state.currentIndex + 1;
|
||||||
|
if (n < this.state.playlist.length) return n;
|
||||||
|
if (this.state.loop && this.state.playlist.length > 0) return 0;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_clearAdvance() {
|
||||||
|
if (this._advanceTimer) { clearTimeout(this._advanceTimer); this._advanceTimer = null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
async skip() {
|
||||||
|
const next = this._nextIndex();
|
||||||
|
if (next === null) { await this.stopChannel(); return this.getStatus(); }
|
||||||
|
await this._playIndex(next);
|
||||||
|
return this.getStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
async pause() {
|
||||||
|
try { await this.amcp.send(`PAUSE ${CHANNEL}-${FG_LAYER}`); } catch (_) {}
|
||||||
|
return this.getStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
async resume() {
|
||||||
|
try { await this.amcp.send(`RESUME ${CHANNEL}-${FG_LAYER}`); } catch (_) {}
|
||||||
|
return this.getStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
_reportAsRunStart(item) {
|
||||||
|
// The mam-api owns the as-run table; the sidecar just logs locally. The API
|
||||||
|
// polls /status and writes as-run rows on clip change. Keeping the DB write
|
||||||
|
// in the API avoids giving the sidecar a DB connection.
|
||||||
|
this.state.currentItemId = item.id || null;
|
||||||
|
this.state.currentItemStartedAt = new Date().toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
getStatus() {
|
||||||
|
return {
|
||||||
|
running: this.state.running,
|
||||||
|
outputType: this.state.outputType,
|
||||||
|
videoFormat: this.state.videoFormat,
|
||||||
|
currentIndex: this.state.currentIndex,
|
||||||
|
currentClip: this.state.currentClip,
|
||||||
|
currentItemId: this.state.currentItemId || null,
|
||||||
|
currentItemStartedAt: this.state.currentItemStartedAt || null,
|
||||||
|
playlistLength: this.state.playlist.length,
|
||||||
|
loop: this.state.loop,
|
||||||
|
startedAt: this.state.startedAt,
|
||||||
|
lastError: this.state.lastError,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new PlayoutManager();
|
||||||
|
|
@ -61,12 +61,29 @@ server {
|
||||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||||
}
|
}
|
||||||
|
|
||||||
# Live HLS — served from /live (bind-mounted shared volume), low cache so playlist refreshes
|
# Live HLS — served from /live (bind-mounted capture live volume).
|
||||||
|
# no-store (not just no-cache): with "no-cache" the browser still caches the
|
||||||
|
# playlist and serves a STALE copy to hls.js's reloads, so hls.js sees the
|
||||||
|
# live playlist as never advancing ("MISSED" forever) and never plays — the
|
||||||
|
# monitor stays black. no-store forbids caching entirely so every reload
|
||||||
|
# fetches the fresh live edge. Segments are short-lived; not caching them is
|
||||||
|
# fine for a live preview.
|
||||||
location /live/ {
|
location /live/ {
|
||||||
alias /live/;
|
alias /live/;
|
||||||
types { application/vnd.apple.mpegurl m3u8; video/mp2t ts; }
|
types { application/vnd.apple.mpegurl m3u8; video/mp2t ts; }
|
||||||
add_header Cache-Control "no-cache";
|
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
|
||||||
add_header Access-Control-Allow-Origin *;
|
add_header Pragma "no-cache" always;
|
||||||
|
add_header Access-Control-Allow-Origin * always;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Playout HLS preview — CasparCG sidecar writes to the media volume under
|
||||||
|
# /media/live/<channel_id>/. This is a separate volume from /live/ (capture).
|
||||||
|
location /media/live/ {
|
||||||
|
alias /media/live/;
|
||||||
|
types { application/vnd.apple.mpegurl m3u8; video/mp2t ts; }
|
||||||
|
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
|
||||||
|
add_header Pragma "no-cache" always;
|
||||||
|
add_header Access-Control-Allow-Origin * always;
|
||||||
}
|
}
|
||||||
|
|
||||||
# API proxy - forward to mam-api service
|
# API proxy - forward to mam-api service
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ function App() {
|
||||||
schedule: ['Ingest', 'Schedule'],
|
schedule: ['Ingest', 'Schedule'],
|
||||||
youtube: ['Ingest', 'YouTube'],
|
youtube: ['Ingest', 'YouTube'],
|
||||||
capture: ['Ingest', 'Capture'], monitors: ['Ingest', 'Monitors'],
|
capture: ['Ingest', 'Capture'], monitors: ['Ingest', 'Monitors'],
|
||||||
jobs: ['Jobs'], editor: ['Editor'],
|
jobs: ['Jobs'], editor: ['Editor'], playout: ['Operations', 'Playout'],
|
||||||
users: ['Admin', 'Users & Groups'], tokens: ['Admin', 'Tokens'],
|
users: ['Admin', 'Users & Groups'], tokens: ['Admin', 'Tokens'],
|
||||||
containers: ['Admin', 'Containers'], cluster: ['Admin', 'Cluster'],
|
containers: ['Admin', 'Containers'], cluster: ['Admin', 'Cluster'],
|
||||||
settings: ['Admin', 'Settings'],
|
settings: ['Admin', 'Settings'],
|
||||||
|
|
@ -120,6 +120,7 @@ function App() {
|
||||||
case 'capture': content = <Capture navigate={navigate} />; break;
|
case 'capture': content = <Capture navigate={navigate} />; break;
|
||||||
case 'monitors': content = <Monitors navigate={navigate} />; break;
|
case 'monitors': content = <Monitors navigate={navigate} />; break;
|
||||||
case 'jobs': content = <Jobs navigate={navigate} />; break;
|
case 'jobs': content = <Jobs navigate={navigate} />; break;
|
||||||
|
case 'playout': content = <Playout navigate={navigate} />; break;
|
||||||
case 'users': content = <Users />; break;
|
case 'users': content = <Users />; break;
|
||||||
case 'tokens': content = <Tokens />; break;
|
case 'tokens': content = <Tokens />; break;
|
||||||
case 'billing': content = <TokensParody />; break;
|
case 'billing': content = <TokensParody />; break;
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,12 @@ window.PREMIERE_RELEASES = [
|
||||||
];
|
];
|
||||||
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: [],
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ const ICONS = {
|
||||||
upload: <><path d="M12 16V4" /><path d="M6 10l6-6 6 6" /><path d="M4 20h16" /></>,
|
upload: <><path d="M12 16V4" /><path d="M6 10l6-6 6 6" /><path d="M4 20h16" /></>,
|
||||||
record: <><rect x="2" y="6" width="14" height="12" rx="2" /><path d="M22 8l-6 4 6 4V8z" /></>,
|
record: <><rect x="2" y="6" width="14" height="12" rx="2" /><path d="M22 8l-6 4 6 4V8z" /></>,
|
||||||
capture: <><circle cx="12" cy="12" r="9" /><circle cx="12" cy="12" r="4" /><circle cx="12" cy="12" r="1" /></>,
|
capture: <><circle cx="12" cy="12" r="9" /><circle cx="12" cy="12" r="4" /><circle cx="12" cy="12" r="1" /></>,
|
||||||
jobs: <><path d="M3 6h18" /><path d="M3 12h18" /><path d="M3 18h12" /></>,
|
jobs: <><path d="M8 6h13M8 12h13M8 18h13" /><circle cx="4" cy="6" r="1" fill="currentColor" /><circle cx="4" cy="12" r="1" fill="currentColor" /><circle cx="4" cy="18" r="1" fill="currentColor" /></>,
|
||||||
editor: <><path d="M14.06 2.94l7 7-11 11H3v-7.06l11.06-10.94z" /><path d="M13 4l7 7" /></>,
|
editor: <><path d="M14.06 2.94l7 7-11 11H3v-7.06l11.06-10.94z" /><path d="M13 4l7 7" /></>,
|
||||||
users: <><circle cx="9" cy="8" r="4" /><path d="M2 21a7 7 0 0 1 14 0" /><circle cx="17" cy="6" r="3" /><path d="M22 18a5 5 0 0 0-7-4.5" /></>,
|
users: <><circle cx="9" cy="8" r="4" /><path d="M2 21a7 7 0 0 1 14 0" /><circle cx="17" cy="6" r="3" /><path d="M22 18a5 5 0 0 0-7-4.5" /></>,
|
||||||
token: <><circle cx="8" cy="15" r="4" /><path d="M10.85 12.15L19 4" /><path d="M18 5l3 3" /><path d="M15 8l3 3" /></>,
|
token: <><circle cx="8" cy="15" r="4" /><path d="M10.85 12.15L19 4" /><path d="M18 5l3 3" /><path d="M15 8l3 3" /></>,
|
||||||
|
|
@ -29,6 +29,7 @@ const ICONS = {
|
||||||
audio: <><path d="M3 12v-2a2 2 0 0 1 2-2h2l5-4v16l-5-4H5a2 2 0 0 1-2-2z" /><path d="M16 8a5 5 0 0 1 0 8M19 5a9 9 0 0 1 0 14" /></>,
|
audio: <><path d="M3 12v-2a2 2 0 0 1 2-2h2l5-4v16l-5-4H5a2 2 0 0 1-2-2z" /><path d="M16 8a5 5 0 0 1 0 8M19 5a9 9 0 0 1 0 14" /></>,
|
||||||
image: <><rect x="3" y="3" width="18" height="18" rx="2" /><circle cx="9" cy="9" r="2" /><path d="M21 15l-5-5L5 21" /></>,
|
image: <><rect x="3" y="3" width="18" height="18" rx="2" /><circle cx="9" cy="9" r="2" /><path d="M21 15l-5-5L5 21" /></>,
|
||||||
download: <><path d="M12 4v12M6 10l6 6 6-6" /><path d="M4 20h16" /></>,
|
download: <><path d="M12 4v12M6 10l6 6 6-6" /><path d="M4 20h16" /></>,
|
||||||
|
import: <><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4" /><path d="M10 17l5-5-5-5" /><path d="M15 12H3" /></>,
|
||||||
key: <><circle cx="7.5" cy="15.5" r="3.5" /><path d="M10 13l9-9M16 7l3 3M14 9l3 3" /></>,
|
key: <><circle cx="7.5" cy="15.5" r="3.5" /><path d="M10 13l9-9M16 7l3 3M14 9l3 3" /></>,
|
||||||
lock: <><rect x="5" y="11" width="14" height="10" rx="2" /><path d="M8 11V7a4 4 0 0 1 8 0v4" /></>,
|
lock: <><rect x="5" y="11" width="14" height="10" rx="2" /><path d="M8 11V7a4 4 0 0 1 8 0v4" /></>,
|
||||||
edit: <><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" /><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" /></>,
|
edit: <><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" /><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" /></>,
|
||||||
|
|
@ -38,14 +39,14 @@ const ICONS = {
|
||||||
x: <path d="M6 6l12 12M6 18L18 6" />,
|
x: <path d="M6 6l12 12M6 18L18 6" />,
|
||||||
filter: <path d="M3 5h18l-7 9v6l-4-2v-4L3 5z" />,
|
filter: <path d="M3 5h18l-7 9v6l-4-2v-4L3 5z" />,
|
||||||
sort: <><path d="M3 6h13M3 12h9M3 18h5" /><path d="M17 14l3 3 3-3M20 9v8" /></>,
|
sort: <><path d="M3 6h13M3 12h9M3 18h5" /><path d="M17 14l3 3 3-3M20 9v8" /></>,
|
||||||
grid: <><rect x="3" y="3" width="7" height="7" /><rect x="14" y="3" width="7" height="7" /><rect x="3" y="14" width="7" height="7" /><rect x="14" y="14" width="7" height="7" /></>,
|
grid: <><rect x="3" y="3" width="7" height="7" rx="1" /><rect x="14" y="3" width="7" height="7" rx="1" /><rect x="3" y="14" width="7" height="7" rx="1" /><rect x="14" y="14" width="7" height="7" rx="1" /></>,
|
||||||
list: <><path d="M8 6h13M8 12h13M8 18h13" /><circle cx="4" cy="6" r="1" fill="currentColor" /><circle cx="4" cy="12" r="1" fill="currentColor" /><circle cx="4" cy="18" r="1" fill="currentColor" /></>,
|
list: <><path d="M8 6h13M8 12h13M8 18h13" /><circle cx="4" cy="6" r="1" fill="currentColor" /><circle cx="4" cy="12" r="1" fill="currentColor" /><circle cx="4" cy="18" r="1" fill="currentColor" /></>,
|
||||||
comment: <path d="M21 11.5a8.4 8.4 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.4 8.4 0 0 1-3.8-.9L3 21l1.9-5.7a8.4 8.4 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.4 8.4 0 0 1 3.8-.9h.5a8.5 8.5 0 0 1 8 8v.5z" />,
|
comment: <path d="M21 11.5a8.4 8.4 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.4 8.4 0 0 1-3.8-.9L3 21l1.9-5.7a8.4 8.4 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.4 8.4 0 0 1 3.8-.9h.5a8.5 8.5 0 0 1 8 8v.5z" />,
|
||||||
clock: <><circle cx="12" cy="12" r="9" /><path d="M12 7v5l3 2" /></>,
|
clock: <><circle cx="12" cy="12" r="9" /><path d="M12 7v5l3 2" /></>,
|
||||||
layers: <><path d="M12 2L2 7l10 5 10-5-10-5z" /><path d="M2 17l10 5 10-5M2 12l10 5 10-5" /></>,
|
layers: <><path d="M12 2L2 7l10 5 10-5-10-5z" /><path d="M2 17l10 5 10-5M2 12l10 5 10-5" /></>,
|
||||||
gpu: <><rect x="3" y="7" width="18" height="10" rx="1" /><rect x="6" y="10" width="4" height="4" /><rect x="14" y="10" width="4" height="4" /><path d="M3 11H1M3 13H1M23 11h-2M23 13h-2" /></>,
|
gpu: <><rect x="3" y="7" width="18" height="10" rx="1" /><rect x="6" y="10" width="4" height="4" /><rect x="14" y="10" width="4" height="4" /><path d="M3 11H1M3 13H1M23 11h-2M23 13h-2" /></>,
|
||||||
cpu: <><rect x="4" y="4" width="16" height="16" rx="2" /><rect x="9" y="9" width="6" height="6" /><path d="M9 1v3M15 1v3M9 20v3M15 20v3M20 9h3M20 14h3M1 9h3M1 14h3" /></>,
|
cpu: <><rect x="4" y="4" width="16" height="16" rx="2" /><rect x="9" y="9" width="6" height="6" /><path d="M9 1v3M15 1v3M9 20v3M15 20v3M20 9h3M20 14h3M1 9h3M1 14h3" /></>,
|
||||||
hdd: <><circle cx="12" cy="12" r="9" /><circle cx="12" cy="12" r="1" fill="currentColor" /></>,
|
hdd: <><ellipse cx="12" cy="6" rx="9" ry="3" /><path d="M3 6v12c0 1.66 4.03 3 9 3s9-1.34 9-3V6" /></>,
|
||||||
sun: <><circle cx="12" cy="12" r="4" /><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4" /></>,
|
sun: <><circle cx="12" cy="12" r="4" /><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4" /></>,
|
||||||
moon: <path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" />,
|
moon: <path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" />,
|
||||||
signal: <><path d="M2 20h.01M7 20v-4M12 20v-8M17 20V8M22 20V4" /></>,
|
signal: <><path d="M2 20h.01M7 20v-4M12 20v-8M17 20V8M22 20V4" /></>,
|
||||||
|
|
@ -66,7 +67,7 @@ const ICONS = {
|
||||||
power: <><path d="M18.36 6.64a9 9 0 1 1-12.73 0" /><path d="M12 2v10" /></>,
|
power: <><path d="M18.36 6.64a9 9 0 1 1-12.73 0" /><path d="M12 2v10" /></>,
|
||||||
globe: <><circle cx="12" cy="12" r="9" /><path d="M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" /></>,
|
globe: <><circle cx="12" cy="12" r="9" /><path d="M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" /></>,
|
||||||
package: <><path d="M3 7l9-4 9 4M3 7v10l9 4 9-4V7M3 7l9 4 9-4M12 11v10" /></>,
|
package: <><path d="M3 7l9-4 9 4M3 7v10l9 4 9-4V7M3 7l9 4 9-4M12 11v10" /></>,
|
||||||
proxy: <><rect x="3" y="3" width="18" height="18" rx="2" /><path d="M9 12l3-3 3 3M12 9v8" /></>,
|
proxy: <><path d="M4 6h11M19 6h1M4 12h2M10 12h10M4 18h7M15 18h5" /><circle cx="17" cy="6" r="2" /><circle cx="8" cy="12" r="2" /><circle cx="13" cy="18" r="2" /></>,
|
||||||
};
|
};
|
||||||
|
|
||||||
function Icon({ name, size = 16, className, style }) {
|
function Icon({ name, size = 16, className, style }) {
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
<link rel="stylesheet" href="styles-rest.css" />
|
<link rel="stylesheet" href="styles-rest.css" />
|
||||||
<link rel="stylesheet" href="styles-modal.css" />
|
<link rel="stylesheet" href="styles-modal.css" />
|
||||||
<link rel="stylesheet" href="styles-fixes.css" />
|
<link rel="stylesheet" href="styles-fixes.css" />
|
||||||
|
<link rel="stylesheet" href="styles-playout.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|
@ -47,6 +48,7 @@
|
||||||
<script src="js/bmd-card.js"></script>
|
<script src="js/bmd-card.js"></script>
|
||||||
<script src="dist/screens-editor.js"></script>
|
<script src="dist/screens-editor.js"></script>
|
||||||
<script src="dist/screens-admin.js"></script>
|
<script src="dist/screens-admin.js"></script>
|
||||||
|
<script src="dist/screens-playout.js"></script>
|
||||||
<script src="dist/modal-new-recorder.js"></script>
|
<script src="dist/modal-new-recorder.js"></script>
|
||||||
<script src="dist/app.js"></script>
|
<script src="dist/app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
|
|
@ -161,6 +161,7 @@ function NewRecorderModal({ open, onClose }) {
|
||||||
const BITRATE_CODECS = new Set(['hevc_nvenc', 'h264_nvenc', 'libx264', 'libx265', 'dnxhd', 'dnxhr_hq']);
|
const BITRATE_CODECS = new Set(['hevc_nvenc', 'h264_nvenc', 'libx264', 'libx265', 'dnxhd', 'dnxhr_hq']);
|
||||||
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 [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);
|
||||||
|
|
@ -206,6 +207,7 @@ function NewRecorderModal({ open, onClose }) {
|
||||||
source_type: sourceType.toLowerCase(),
|
source_type: sourceType.toLowerCase(),
|
||||||
project_id: projectId || undefined,
|
project_id: projectId || undefined,
|
||||||
generate_proxy: proxyOn,
|
generate_proxy: proxyOn,
|
||||||
|
growing_enabled: growingOn,
|
||||||
recording_codec: recCodec,
|
recording_codec: recCodec,
|
||||||
recording_container: recContainer,
|
recording_container: recContainer,
|
||||||
// Framerate + resolution are auto-detected from the source signal/stream.
|
// Framerate + resolution are auto-detected from the source signal/stream.
|
||||||
|
|
@ -473,6 +475,20 @@ function NewRecorderModal({ open, onClose }) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="modal-toggle-row">
|
||||||
|
<label className="switch">
|
||||||
|
<input type="checkbox" checked={growingOn} onChange={e => setGrowingOn(e.target.checked)} />
|
||||||
|
<span className="switch-track"><span className="switch-knob" /></span>
|
||||||
|
</label>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontWeight: 500, fontSize: 13 }}>Growing-files mode</div>
|
||||||
|
<div style={{ fontSize: 11, color: 'var(--text-3)' }}>
|
||||||
|
Write the live master to the SMB share so editors can cut while it's still recording.
|
||||||
|
Requires the SMB share to be configured in Settings → Storage.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{proxyOn && (
|
{proxyOn && (
|
||||||
<div className="modal-section">
|
<div className="modal-section">
|
||||||
<div className="modal-section-head"><span>Proxy</span></div>
|
<div className="modal-section-head"><span>Proxy</span></div>
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,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 +45,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,
|
||||||
|
|
@ -258,28 +266,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 +286,204 @@ 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 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 = (u) => {
|
||||||
|
if (!confirm(`Reset two-factor for "${u.name}" (@${u.username})?\nThey will be able to sign in without a code until they re-enrol.`)) 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>
|
||||||
|
{/* 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);
|
||||||
|
|
@ -992,6 +1177,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.
|
||||||
// ────────────────────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -1181,18 +1508,8 @@ function Cluster() {
|
||||||
|
|
||||||
const [adviceModal, setAdviceModal] = React.useState(null); // {title, lines:[], commands?}
|
const [adviceModal, setAdviceModal] = React.useState(null); // {title, lines:[], commands?}
|
||||||
|
|
||||||
const addNode = () => setAdviceModal({
|
const [showAddNode, setShowAddNode] = React.useState(false);
|
||||||
title: 'Add a worker node',
|
const addNode = () => setShowAddNode(true);
|
||||||
lines: [
|
|
||||||
'Worker nodes auto-register with the cluster on first heartbeat.',
|
|
||||||
'Run these on the new host (replace 10.0.0.25 with this MAM\'s IP):',
|
|
||||||
],
|
|
||||||
commands: [
|
|
||||||
'git clone https://forge.wilddragon.net/zgaetano/dragonflight.git /opt/dragonflight',
|
|
||||||
'cd /opt/dragonflight && cp .env.example .env && nano .env # set NODE_ROLE=worker, point at MAM IP',
|
|
||||||
'docker compose -f docker-compose.worker.yml up -d',
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const drainNode = (node) => setAdviceModal({
|
const drainNode = (node) => setAdviceModal({
|
||||||
title: `Drain ${node.id}`,
|
title: `Drain ${node.id}`,
|
||||||
|
|
@ -1389,6 +1706,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>
|
||||||
|
|
@ -1399,6 +1719,7 @@ function Cluster() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{showAddNode && <AddNodeModal onClose={() => setShowAddNode(false)} />}
|
||||||
{adviceModal && (
|
{adviceModal && (
|
||||||
<div className="modal-backdrop" onClick={() => setAdviceModal(null)}>
|
<div className="modal-backdrop" onClick={() => setAdviceModal(null)}>
|
||||||
<div className="modal" style={{ width: 560 }} onClick={e => e.stopPropagation()}>
|
<div className="modal" style={{ width: 560 }} onClick={e => e.stopPropagation()}>
|
||||||
|
|
@ -1429,6 +1750,142 @@ function Cluster() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddNodeModal — Approach A onboarding wizard. Collects a node name, mints a
|
||||||
|
// one-time auth token via /auth/tokens, and renders a ready-to-paste
|
||||||
|
// `curl … | bash` command that provisions the machine via deploy/onboard-node.sh.
|
||||||
|
//
|
||||||
|
// No role picker: the new node self-detects its hardware (GPU / DeckLink /
|
||||||
|
// Deltacast) in onboard-node.sh and auto-enables the matching compose profiles
|
||||||
|
// (worker always; + gpu / + capture when present). Zero manual choice.
|
||||||
|
function AddNodeModal({ onClose }) {
|
||||||
|
const [nodeName, setNodeName] = React.useState('');
|
||||||
|
const [apiUrl, setApiUrl] = React.useState('');
|
||||||
|
const [info, setInfo] = React.useState(null); // { scriptUrl, branch }
|
||||||
|
const [command, setCommand] = React.useState(null); // generated string
|
||||||
|
const [error, setError] = React.useState(null);
|
||||||
|
const [busy, setBusy] = React.useState(false);
|
||||||
|
const [copied, setCopied] = React.useState(false);
|
||||||
|
|
||||||
|
// On open, prefill the editable apiUrl + capture scriptUrl/branch.
|
||||||
|
React.useEffect(() => {
|
||||||
|
window.ZAMPP_API.fetch('/cluster/onboard-info')
|
||||||
|
.then(d => {
|
||||||
|
setInfo({ scriptUrl: d.scriptUrl, branch: d.branch });
|
||||||
|
if (d.apiUrl) setApiUrl(d.apiUrl);
|
||||||
|
})
|
||||||
|
.catch(() => {}); // leave apiUrl empty → user must fill it before Generate
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const generate = async () => {
|
||||||
|
setError(null);
|
||||||
|
if (!nodeName.trim()) { setError('Node name is required.'); return; }
|
||||||
|
if (!apiUrl.trim()) { setError('Primary API URL is required.'); return; }
|
||||||
|
setBusy(true);
|
||||||
|
try {
|
||||||
|
const r = await fetch('/api/v1/auth/tokens', {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'dragonflight-ui' },
|
||||||
|
body: JSON.stringify({ name: 'node: ' + nodeName.trim() }),
|
||||||
|
});
|
||||||
|
if (r.status !== 201) {
|
||||||
|
const body = await r.json().catch(() => ({}));
|
||||||
|
setError(body.error || ('Failed to mint token (' + r.status + ')'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { token } = await r.json();
|
||||||
|
const scriptUrl = (info && info.scriptUrl)
|
||||||
|
|| 'https://forge.wilddragon.net/zgaetano/wild-dragon/raw/branch/main/deploy/onboard-node.sh';
|
||||||
|
const cmd =
|
||||||
|
`curl -sL ${scriptUrl} | NODE_TOKEN=${token} MAM_API_URL=${apiUrl.trim()} bash`;
|
||||||
|
setCommand(cmd);
|
||||||
|
} catch (e) {
|
||||||
|
setError(e.message || 'Network error');
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const copy = () => {
|
||||||
|
if (!command || !navigator.clipboard) return;
|
||||||
|
navigator.clipboard.writeText(command)
|
||||||
|
.then(() => { setCopied(true); setTimeout(() => setCopied(false), 2000); })
|
||||||
|
.catch(() => {});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="modal-backdrop" onClick={onClose}>
|
||||||
|
<div className="modal" style={{ width: 620 }} onClick={e => e.stopPropagation()}>
|
||||||
|
<div className="modal-head">
|
||||||
|
<div style={{ fontSize: 15, fontWeight: 600 }}>Add cluster node</div>
|
||||||
|
<button className="icon-btn" aria-label="Close" onClick={onClose}><Icon name="x" /></button>
|
||||||
|
</div>
|
||||||
|
<div className="modal-body">
|
||||||
|
{!command && (
|
||||||
|
<React.Fragment>
|
||||||
|
<div style={{ marginBottom: 14 }}>
|
||||||
|
<label style={{ display: 'block', fontSize: 11.5, color: 'var(--text-3)', marginBottom: 5 }}>Node name</label>
|
||||||
|
<input className="field-input" style={{ width: '100%' }} autoFocus
|
||||||
|
placeholder="e.g. zampp3"
|
||||||
|
value={nodeName} onChange={e => setNodeName(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: 4 }}>
|
||||||
|
<label style={{ display: 'block', fontSize: 11.5, color: 'var(--text-3)', marginBottom: 5 }}>Primary API URL</label>
|
||||||
|
<input className="field-input mono" style={{ width: '100%', fontSize: 12 }}
|
||||||
|
placeholder="http://10.0.0.25:47432"
|
||||||
|
value={apiUrl} onChange={e => setApiUrl(e.target.value)} />
|
||||||
|
<div style={{ fontSize: 11, color: 'var(--text-4)', marginTop: 4 }}>
|
||||||
|
The LAN address this new node will heartbeat to. Edit if the guess is wrong.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{command && (
|
||||||
|
<React.Fragment>
|
||||||
|
<div style={{ marginBottom: 10, padding: 10, border: '1px solid var(--accent)', background: 'var(--accent-soft)', borderRadius: 6 }}>
|
||||||
|
<div style={{ fontSize: 11, fontWeight: 600, color: 'var(--accent-text)' }}>
|
||||||
|
This token is shown only once — copy the command now.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<code className="mono" style={{ display: 'block', background: 'var(--bg-2)', padding: 12, borderRadius: 6, fontSize: 11.5, lineHeight: 1.5, wordBreak: 'break-all', whiteSpace: 'pre-wrap' }}>{command}</code>
|
||||||
|
<div style={{ fontSize: 11, color: 'var(--text-4)', marginTop: 8, lineHeight: 1.5 }}>
|
||||||
|
Profiles (worker / capture / GPU) are auto-selected from the new machine's detected hardware — no need to choose.
|
||||||
|
</div>
|
||||||
|
<ol style={{ margin: '12px 0 0', paddingLeft: 18, fontSize: 12, color: 'var(--text-2)', lineHeight: 1.6 }}>
|
||||||
|
<li>SSH into the fresh Ubuntu machine.</li>
|
||||||
|
<li>Paste and run this command.</li>
|
||||||
|
<li>The node appears in this Cluster view within ~30s.</li>
|
||||||
|
</ol>
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div style={{ marginTop: 10, fontSize: 11.5, color: 'var(--danger)' }}>{error}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="modal-foot">
|
||||||
|
{!command && (
|
||||||
|
<React.Fragment>
|
||||||
|
<button className="btn ghost sm" onClick={onClose}>Cancel</button>
|
||||||
|
<button className="btn primary sm" disabled={busy} onClick={generate}>
|
||||||
|
{busy ? 'Generating…' : 'Generate command'}
|
||||||
|
</button>
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
{command && (
|
||||||
|
<React.Fragment>
|
||||||
|
<button className="btn ghost sm" onClick={copy}>{copied ? 'Copied' : 'Copy'}</button>
|
||||||
|
<button className="btn primary sm" onClick={onClose}>Done</button>
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function DetailRow({ k, v, mono }) {
|
function DetailRow({ k, v, mono }) {
|
||||||
return (
|
return (
|
||||||
<div style={{ display: "grid", gridTemplateColumns: "90px 1fr", alignItems: "center", fontSize: 12 }}>
|
<div style={{ display: "grid", gridTemplateColumns: "90px 1fr", alignItems: "center", fontSize: 12 }}>
|
||||||
|
|
@ -1749,6 +2206,7 @@ function Settings() {
|
||||||
function StorageSection() {
|
function StorageSection() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<StorageWarningBanner />
|
||||||
<MountHealthStrip />
|
<MountHealthStrip />
|
||||||
<S3SettingsCard />
|
<S3SettingsCard />
|
||||||
<GrowingSettingsCard />
|
<GrowingSettingsCard />
|
||||||
|
|
@ -1756,6 +2214,27 @@ function StorageSection() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set-once deployment warning. Storage paths are written into asset rows and
|
||||||
|
// the S3 layout at ingest time; changing them after assets exist orphans files
|
||||||
|
// and can corrupt the library's view of where masters/proxies live.
|
||||||
|
function StorageWarningBanner() {
|
||||||
|
return (
|
||||||
|
<div role="alert" style={{
|
||||||
|
display: 'flex', alignItems: 'flex-start', gap: 12,
|
||||||
|
padding: '14px 16px', marginBottom: 14, borderRadius: 10,
|
||||||
|
border: '1px solid var(--danger)',
|
||||||
|
background: 'color-mix(in srgb, var(--danger) 12%, transparent)',
|
||||||
|
}}>
|
||||||
|
<Icon name="alert" size={20} style={{ color: 'var(--danger)', flexShrink: 0, marginTop: 1 }} />
|
||||||
|
<div style={{ fontSize: 13, fontWeight: 700, letterSpacing: '0.02em', lineHeight: 1.5, color: 'var(--text-1)' }}>
|
||||||
|
WARNING — THESE SETTINGS ARE MEANT TO BE SET ONCE AT INITIAL DEPLOYMENT.
|
||||||
|
CHANGING STORAGE PATHS MID-CYCLE CAN CAUSE DATABASE CORRUPTION AND FILE LOSS.
|
||||||
|
PLEASE USE WITH CAUTION.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function formatBytes(n) {
|
function formatBytes(n) {
|
||||||
if (n == null || isNaN(n)) return '·';
|
if (n == null || isNaN(n)) return '·';
|
||||||
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
||||||
|
|
@ -1828,8 +2307,8 @@ function MountHealthStrip() {
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
||||||
<strong style={{ fontSize: 12.5 }}>Growing files</strong>
|
<strong style={{ fontSize: 12.5 }}>Growing files</strong>
|
||||||
{g.enabled
|
{g.enabled
|
||||||
? <HealthPill ok={growingHealthy} label={growingHealthy ? 'mounted' : 'unreachable'} detail={g.error || ''} />
|
? <HealthPill ok={growingHealthy} label={growingHealthy ? 'configured' : 'unreachable'} detail={g.error || ''} />
|
||||||
: <span className="badge neutral">disabled</span>}
|
: <span className="badge neutral">not configured</span>}
|
||||||
{g.enabled && g.exists && (
|
{g.enabled && g.exists && (
|
||||||
<HealthPill ok={g.writable} label={g.writable ? 'writable' : 'read-only'} />
|
<HealthPill ok={g.writable} label={g.writable ? 'writable' : 'read-only'} />
|
||||||
)}
|
)}
|
||||||
|
|
@ -1842,7 +2321,8 @@ function MountHealthStrip() {
|
||||||
<div style={{ fontSize: 11.5, color: 'var(--text-3)', display: 'grid', gridTemplateColumns: 'auto 1fr', columnGap: 10, rowGap: 2 }}>
|
<div style={{ fontSize: 11.5, color: 'var(--text-3)', display: 'grid', gridTemplateColumns: 'auto 1fr', columnGap: 10, rowGap: 2 }}>
|
||||||
<span>Container</span><span className="mono">{g.container_path || '·'}</span>
|
<span>Container</span><span className="mono">{g.container_path || '·'}</span>
|
||||||
<span>Host</span><span className="mono">{g.host_path || '·'}</span>
|
<span>Host</span><span className="mono">{g.host_path || '·'}</span>
|
||||||
<span>SMB</span><span className="mono">{g.smb_url || '·'}</span>
|
<span>SMB mount</span><span className="mono">{g.smb_mount || '·'}</span>
|
||||||
|
<span>SMB (editors)</span><span className="mono">{g.smb_url || '·'}</span>
|
||||||
<span>Promote idle</span><span className="mono">{g.promote_after_seconds}s</span>
|
<span>Promote idle</span><span className="mono">{g.promote_after_seconds}s</span>
|
||||||
{g.error && <><span>Error</span><span style={{ color: 'var(--danger)' }}>{g.error}</span></>}
|
{g.error && <><span>Error</span><span style={{ color: 'var(--danger)' }}>{g.error}</span></>}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -2036,35 +2516,75 @@ function GpuSettingsCard() {
|
||||||
|
|
||||||
function GrowingSettingsCard() {
|
function GrowingSettingsCard() {
|
||||||
const [cfg, setCfg] = React.useState(null);
|
const [cfg, setCfg] = React.useState(null);
|
||||||
|
const [pwd, setPwd] = React.useState(''); // new password to set; '' = leave unchanged
|
||||||
|
const [pwdExists, setPwdExists] = React.useState(false);
|
||||||
|
const [clearPwd, setClearPwd] = React.useState(false);
|
||||||
const [saving, setSaving] = React.useState(false);
|
const [saving, setSaving] = React.useState(false);
|
||||||
const [msg, setMsg] = React.useState(null);
|
const [msg, setMsg] = React.useState(null);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
window.ZAMPP_API.fetch('/settings/growing').then(setCfg).catch(() => setCfg({
|
window.ZAMPP_API.fetch('/settings/growing')
|
||||||
growing_enabled: 'false', growing_path: '/growing', growing_smb_url: '', growing_promote_after_seconds: '8',
|
.then(d => { setCfg(d); setPwdExists(!!d.growing_smb_password_exists); })
|
||||||
}));
|
.catch(() => setCfg({
|
||||||
|
growing_path: '/growing', growing_smb_url: '', growing_smb_mount: '',
|
||||||
|
growing_smb_username: '', growing_smb_vers: '3.0', growing_promote_after_seconds: '8',
|
||||||
|
}));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const save = () => {
|
const save = () => {
|
||||||
setSaving(true); setMsg(null);
|
setSaving(true); setMsg(null);
|
||||||
window.ZAMPP_API.fetch('/settings/growing', { method: 'PUT', body: JSON.stringify(cfg) })
|
const body = {
|
||||||
.then(() => { setSaving(false); setMsg({ ok: true, text: 'Saved.' }); })
|
growing_path: cfg.growing_path,
|
||||||
|
growing_smb_url: cfg.growing_smb_url,
|
||||||
|
growing_smb_mount: cfg.growing_smb_mount,
|
||||||
|
growing_smb_username: cfg.growing_smb_username,
|
||||||
|
growing_smb_vers: cfg.growing_smb_vers,
|
||||||
|
growing_promote_after_seconds: cfg.growing_promote_after_seconds,
|
||||||
|
};
|
||||||
|
if (clearPwd) body.growing_smb_password_clear = true;
|
||||||
|
else if (pwd) body.growing_smb_password = pwd;
|
||||||
|
window.ZAMPP_API.fetch('/settings/growing', { method: 'PUT', body: JSON.stringify(body) })
|
||||||
|
.then(() => {
|
||||||
|
setSaving(false); setMsg({ ok: true, text: 'Saved.' });
|
||||||
|
if (clearPwd) { setPwdExists(false); setClearPwd(false); }
|
||||||
|
else if (pwd) { setPwdExists(true); setPwd(''); }
|
||||||
|
})
|
||||||
.catch(e => { setSaving(false); setMsg({ ok: false, text: e.message }); });
|
.catch(e => { setSaving(false); setMsg({ ok: false, text: e.message }); });
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!cfg) return <SettingsCard icon="hdd" title="Growing files (SMB)" sub="Loading…"><div style={{color:'var(--text-3)'}}>…</div></SettingsCard>;
|
if (!cfg) return <SettingsCard icon="hdd" title="Growing files (SMB)" sub="Loading…"><div style={{color:'var(--text-3)'}}>…</div></SettingsCard>;
|
||||||
const set = (k, v) => setCfg(c => ({ ...c, [k]: v }));
|
const set = (k, v) => setCfg(c => ({ ...c, [k]: v }));
|
||||||
const enabled = cfg.growing_enabled === 'true' || cfg.growing_enabled === true;
|
const mountConfigured = !!(cfg.growing_smb_mount && cfg.growing_smb_mount.trim());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsCard icon="hdd" title="Growing files (SMB)" sub="High-speed local landing zone; promote to S3 on stop"
|
<SettingsCard icon="hdd" title="Growing files (SMB)" sub="Shared SMB landing zone. Enable per-recorder under Ingest → Recorders."
|
||||||
tag={enabled ? <span className="badge success">enabled</span> : <span className="badge neutral">disabled</span>}>
|
tag={mountConfigured ? <span className="badge success">configured</span> : <span className="badge neutral">not configured</span>}>
|
||||||
<form onSubmit={e => { e.preventDefault(); save(); }} autoComplete="off">
|
<form onSubmit={e => { e.preventDefault(); save(); }} autoComplete="off">
|
||||||
<SField label="Enable growing-file capture">
|
<div style={{ fontSize: 11.5, color: 'var(--text-3)', marginBottom: 10, lineHeight: 1.5 }}>
|
||||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 12.5 }}>
|
Growing-file mode is enabled <strong style={{ color: 'var(--text-2)' }}>per recorder</strong> (New recorder → Growing-files mode).
|
||||||
<input type="checkbox" checked={enabled} onChange={e => set('growing_enabled', String(e.target.checked))} />
|
These settings describe the SMB share that capture mounts and writes the live master to.
|
||||||
<span style={{ color: 'var(--text-2)' }}>Capture writes to the local SMB share first; Premier can edit while it's still growing.</span>
|
</div>
|
||||||
</label>
|
<SField label="SMB mount source (CIFS)">
|
||||||
|
<input className="field-input mono" value={cfg.growing_smb_mount || ''} onChange={e => set('growing_smb_mount', e.target.value)} placeholder="//10.0.0.25/mam-growing" />
|
||||||
|
</SField>
|
||||||
|
<SField label="SMB username">
|
||||||
|
<input className="field-input mono" value={cfg.growing_smb_username || ''} onChange={e => set('growing_smb_username', e.target.value)} placeholder="capture" autoComplete="off" />
|
||||||
|
</SField>
|
||||||
|
<SField label="SMB password">
|
||||||
|
<input className="field-input mono" type="password" autoComplete="new-password"
|
||||||
|
value={pwd}
|
||||||
|
disabled={clearPwd}
|
||||||
|
onChange={e => setPwd(e.target.value)}
|
||||||
|
placeholder={pwdExists ? '•••••••• (saved — leave blank to keep)' : 'Enter SMB password'} />
|
||||||
|
{pwdExists && (
|
||||||
|
<label style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 11.5, color: 'var(--text-3)', marginTop: 4 }}>
|
||||||
|
<input type="checkbox" checked={clearPwd} onChange={e => { setClearPwd(e.target.checked); if (e.target.checked) setPwd(''); }} />
|
||||||
|
Remove saved password
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
</SField>
|
||||||
|
<SField label="CIFS protocol version">
|
||||||
|
<input className="field-input mono" value={cfg.growing_smb_vers || ''} onChange={e => set('growing_smb_vers', e.target.value)} placeholder="3.0" />
|
||||||
</SField>
|
</SField>
|
||||||
<SField label="Container mount path">
|
<SField label="Container mount path">
|
||||||
<input className="field-input mono" value={cfg.growing_path || ''} onChange={e => set('growing_path', e.target.value)} placeholder="/growing" />
|
<input className="field-input mono" value={cfg.growing_path || ''} onChange={e => set('growing_path', e.target.value)} placeholder="/growing" />
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -997,6 +997,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 +1009,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 +1038,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';
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ function Jobs({ navigate }) {
|
||||||
|
|
||||||
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' };
|
||||||
const kindMap = { proxy: 'Proxy', thumbnail: 'Thumbnail', conform: 'Conform', transcode: 'Transcode', import: 'YouTube' };
|
const kindMap = { proxy: 'Proxy', thumbnail: 'Thumbnail', conform: 'Conform', transcode: 'Transcode', import: 'YouTube', 'playout-stage': 'Stage' };
|
||||||
const meta = j.metadata || {};
|
const meta = j.metadata || {};
|
||||||
return {
|
return {
|
||||||
...j,
|
...j,
|
||||||
|
|
@ -207,7 +207,7 @@ function Jobs({ navigate }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function JobRow({ job, onRetry, onDelete }) {
|
function JobRow({ job, onRetry, onDelete }) {
|
||||||
const iconMap = { Proxy: 'proxy', Transcode: 'film', Thumbnail: 'image', Conform: 'layers' };
|
const iconMap = { Proxy: 'proxy', Transcode: 'film', Thumbnail: 'image', Conform: 'layers', Stage: 'monitor' };
|
||||||
return (
|
return (
|
||||||
<div className="job-row">
|
<div className="job-row">
|
||||||
<div><StatusDot status={job.status} /></div>
|
<div><StatusDot status={job.status} /></div>
|
||||||
|
|
|
||||||
729
services/web-ui/public/screens-playout.jsx
Normal file
729
services/web-ui/public/screens-playout.jsx
Normal file
|
|
@ -0,0 +1,729 @@
|
||||||
|
// screens-playout.jsx — Master Control (MCR) playout page.
|
||||||
|
//
|
||||||
|
// Operator workflow (Phase A — playlist player):
|
||||||
|
// 1. Create / pick a channel (output target: SRT / RTMP / NDI / DeckLink).
|
||||||
|
// 2. Start the channel → spawns the CasparCG sidecar, brings up the output.
|
||||||
|
// 3. Drag assets from the media bin into the playlist; reorder by dragging.
|
||||||
|
// Each item stages from S3 to the CasparCG /media volume in the background.
|
||||||
|
// 4. Hit PLAY → the engine walks the playlist gaplessly. PAUSE / SKIP / STOP
|
||||||
|
// transport. As-run log records what aired.
|
||||||
|
//
|
||||||
|
// Talks to /api/v1/playout via window.ZAMPP_API.fetch. Native HTML5 drag-drop,
|
||||||
|
// no extra library. Components are plain globals (esbuild bundle:false).
|
||||||
|
|
||||||
|
const PO_OUTPUTS = [
|
||||||
|
{ value: 'srt', label: 'SRT' },
|
||||||
|
{ value: 'rtmp', label: 'RTMP' },
|
||||||
|
{ value: 'ndi', label: 'NDI' },
|
||||||
|
{ value: 'decklink', label: 'SDI (DeckLink)' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const PO_FORMATS = ['1080p5994', '1080i5994', '1080p2997', '720p5994', '1080i50', '1080p25'];
|
||||||
|
|
||||||
|
async function poFetch(path, opts) {
|
||||||
|
return window.ZAMPP_API.fetch('/playout' + path, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function fmtDuration(secs) {
|
||||||
|
if (!secs || secs < 0) return '—';
|
||||||
|
const s = Math.floor(secs);
|
||||||
|
const h = Math.floor(s / 3600);
|
||||||
|
const m = Math.floor((s % 3600) / 60);
|
||||||
|
const ss = s % 60;
|
||||||
|
const mm = String(m).padStart(2, '0');
|
||||||
|
const ssStr = String(ss).padStart(2, '0');
|
||||||
|
return h > 0 ? `${h}:${mm}:${ssStr}` : `${m}:${ssStr}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function itemEffectiveDuration(it) {
|
||||||
|
const total = (it.asset_duration_ms || 0) / 1000;
|
||||||
|
const inPt = it.in_point != null ? Number(it.in_point) : 0;
|
||||||
|
const outPt = it.out_point != null ? Number(it.out_point) : total;
|
||||||
|
return Math.max(0, outPt - inPt);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Output-config sub-form (varies by output type) ───────────────────────────
|
||||||
|
function OutputConfigFields({ type, config, onChange }) {
|
||||||
|
const set = (k, v) => onChange({ ...config, [k]: v });
|
||||||
|
if (type === 'decklink') {
|
||||||
|
return (
|
||||||
|
<div className="field">
|
||||||
|
<label className="field-label">DeckLink device index</label>
|
||||||
|
<input className="field-input" type="number" min="1" value={config.device_index || 1}
|
||||||
|
onChange={e => set('device_index', parseInt(e.target.value, 10) || 1)} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (type === 'ndi') {
|
||||||
|
return (
|
||||||
|
<div className="field">
|
||||||
|
<label className="field-label">NDI source name</label>
|
||||||
|
<input className="field-input" value={config.ndi_name || ''} placeholder="DRAGONFLIGHT CH1"
|
||||||
|
onChange={e => set('ndi_name', e.target.value)} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// srt / rtmp
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<div className="field">
|
||||||
|
<label className="field-label">{type.toUpperCase()} URL</label>
|
||||||
|
<input className="field-input mono" value={config.url || ''}
|
||||||
|
placeholder={type === 'srt' ? 'srt://host:9000' : 'rtmp://host/live'}
|
||||||
|
onChange={e => set('url', e.target.value)} />
|
||||||
|
</div>
|
||||||
|
{type === 'rtmp' && (
|
||||||
|
<div className="field">
|
||||||
|
<label className="field-label">Stream key</label>
|
||||||
|
<input className="field-input mono" value={config.key || ''}
|
||||||
|
onChange={e => set('key', e.target.value)} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{type === 'srt' && (
|
||||||
|
<div className="field">
|
||||||
|
<label className="field-label">Latency (ms)</label>
|
||||||
|
<input className="field-input" type="number" value={config.latency || 200}
|
||||||
|
onChange={e => set('latency', parseInt(e.target.value, 10) || 200)} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Channel create modal ─────────────────────────────────────────────────────
|
||||||
|
function ChannelCreate({ onClose, onCreated }) {
|
||||||
|
const PROJECTS = window.ZAMPP_DATA?.PROJECTS || [];
|
||||||
|
const [name, setName] = React.useState('');
|
||||||
|
const [outputType, setOutputType] = React.useState('srt');
|
||||||
|
const [config, setConfig] = React.useState({});
|
||||||
|
const [videoFormat, setVideoFormat] = React.useState('1080i5994');
|
||||||
|
const [projectId, setProjectId] = React.useState(PROJECTS[0]?.id || '');
|
||||||
|
const [busy, setBusy] = React.useState(false);
|
||||||
|
const [err, setErr] = React.useState(null);
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
setBusy(true); setErr(null);
|
||||||
|
try {
|
||||||
|
const ch = await poFetch('/channels', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
name, output_type: outputType, output_config: config,
|
||||||
|
video_format: videoFormat, project_id: projectId || null,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
onCreated(ch);
|
||||||
|
} catch (e) { setErr(e.message || 'Failed to create channel'); }
|
||||||
|
finally { setBusy(false); }
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="modal-backdrop" onClick={onClose}>
|
||||||
|
<div className="modal" onClick={e => e.stopPropagation()} style={{ maxWidth: 460 }}>
|
||||||
|
<div className="modal-header"><h3>New Playout Channel</h3></div>
|
||||||
|
<div className="modal-body">
|
||||||
|
<div className="field">
|
||||||
|
<label className="field-label">Name</label>
|
||||||
|
<input className="field-input" value={name} autoFocus
|
||||||
|
onChange={e => setName(e.target.value)} placeholder="Channel 1" />
|
||||||
|
</div>
|
||||||
|
<div className="field">
|
||||||
|
<label className="field-label">Output</label>
|
||||||
|
<select className="field-input" value={outputType}
|
||||||
|
onChange={e => { setOutputType(e.target.value); setConfig({}); }}>
|
||||||
|
{PO_OUTPUTS.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<OutputConfigFields type={outputType} config={config} onChange={setConfig} />
|
||||||
|
<div className="field">
|
||||||
|
<label className="field-label">Video format</label>
|
||||||
|
<select className="field-input" value={videoFormat} onChange={e => setVideoFormat(e.target.value)}>
|
||||||
|
{PO_FORMATS.map(f => <option key={f} value={f}>{f}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="field">
|
||||||
|
<label className="field-label">Project (RBAC scope)</label>
|
||||||
|
<select className="field-input" value={projectId} onChange={e => setProjectId(e.target.value)}>
|
||||||
|
<option value="">— admin only —</option>
|
||||||
|
{PROJECTS.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{err && <div className="alert error">{err}</div>}
|
||||||
|
</div>
|
||||||
|
<div className="modal-footer">
|
||||||
|
<button className="btn ghost" onClick={onClose}>Cancel</button>
|
||||||
|
<button className="btn primary" disabled={busy || !name} onClick={submit}>
|
||||||
|
{busy ? 'Creating…' : 'Create'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Media bin: assets draggable into the playlist ────────────────────────────
|
||||||
|
function MediaBin({ projectId }) {
|
||||||
|
const ASSETS = (window.ZAMPP_DATA?.ASSETS || []).filter(a =>
|
||||||
|
!projectId || a.project_id === projectId);
|
||||||
|
const [q, setQ] = React.useState('');
|
||||||
|
const filtered = ASSETS.filter(a => !q || (a.name || '').toLowerCase().includes(q.toLowerCase()));
|
||||||
|
|
||||||
|
const onDragStart = (e, asset) => {
|
||||||
|
e.dataTransfer.setData('application/x-df-asset', JSON.stringify({ id: asset.id, name: asset.name }));
|
||||||
|
e.dataTransfer.effectAllowed = 'copy';
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="panel po-bin">
|
||||||
|
<div className="po-bin-head">
|
||||||
|
<span className="po-section-label">Media Bin</span>
|
||||||
|
<input className="field-input sm" placeholder="Filter…" value={q}
|
||||||
|
onChange={e => setQ(e.target.value)} style={{ maxWidth: 160 }} />
|
||||||
|
</div>
|
||||||
|
<div className="po-bin-list">
|
||||||
|
{filtered.length === 0 && <div className="muted" style={{ padding: 12 }}>No assets.</div>}
|
||||||
|
{filtered.map(a => (
|
||||||
|
<div key={a.id} className="po-bin-item" draggable
|
||||||
|
onDragStart={e => onDragStart(e, a)} title="Drag into the playlist">
|
||||||
|
<span className="po-bin-name">{a.name}</span>
|
||||||
|
<span className="mono muted" style={{ fontSize: 11 }}>{a.duration || ''}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Staging progress bar ──────────────────────────────────────────────────────
|
||||||
|
function StagingBar({ status }) {
|
||||||
|
return (
|
||||||
|
<div className={'po-staging-bar po-staging-bar--' + (status || 'pending')} aria-hidden="true" />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Playlist: ordered, drag-drop reorder, drop-target for bin assets ─────────
|
||||||
|
function Playlist({ channel, playlistId, items, activeIndex, onReload }) {
|
||||||
|
const [dragIndex, setDragIndex] = React.useState(null);
|
||||||
|
const [dropErr, setDropErr] = React.useState(null);
|
||||||
|
|
||||||
|
const onItemDragStart = (e, index) => {
|
||||||
|
setDragIndex(index);
|
||||||
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
|
};
|
||||||
|
const onItemDragOver = (e) => { e.preventDefault(); };
|
||||||
|
|
||||||
|
const onItemDrop = async (e, index) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation(); // prevent bubble to onContainerDrop
|
||||||
|
setDropErr(null);
|
||||||
|
const assetRaw = e.dataTransfer.getData('application/x-df-asset');
|
||||||
|
if (assetRaw) {
|
||||||
|
try {
|
||||||
|
const asset = JSON.parse(assetRaw);
|
||||||
|
await poFetch('/playlists/' + playlistId + '/items', {
|
||||||
|
method: 'POST', body: JSON.stringify({ asset_id: asset.id }),
|
||||||
|
});
|
||||||
|
onReload();
|
||||||
|
} catch (err) { setDropErr(err.message || 'Failed to add clip'); }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Reorder within the playlist.
|
||||||
|
if (dragIndex === null || dragIndex === index) return;
|
||||||
|
const order = items.map(i => i.id);
|
||||||
|
const [moved] = order.splice(dragIndex, 1);
|
||||||
|
order.splice(index, 0, moved);
|
||||||
|
setDragIndex(null);
|
||||||
|
try {
|
||||||
|
await poFetch('/playlists/' + playlistId + '/reorder', {
|
||||||
|
method: 'PUT', body: JSON.stringify({ order }),
|
||||||
|
});
|
||||||
|
onReload();
|
||||||
|
} catch (err) { setDropErr(err.message || 'Failed to reorder'); }
|
||||||
|
};
|
||||||
|
|
||||||
|
const onContainerDrop = async (e) => {
|
||||||
|
const assetRaw = e.dataTransfer.getData('application/x-df-asset');
|
||||||
|
if (!assetRaw) return;
|
||||||
|
e.preventDefault();
|
||||||
|
setDropErr(null);
|
||||||
|
try {
|
||||||
|
const asset = JSON.parse(assetRaw);
|
||||||
|
await poFetch('/playlists/' + playlistId + '/items', {
|
||||||
|
method: 'POST', body: JSON.stringify({ asset_id: asset.id }),
|
||||||
|
});
|
||||||
|
onReload();
|
||||||
|
} catch (err) { setDropErr(err.message || 'Failed to add clip'); }
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeItem = async (id) => {
|
||||||
|
try { await poFetch('/items/' + id, { method: 'DELETE' }); onReload(); }
|
||||||
|
catch (err) { setDropErr(err.message || 'Failed to remove'); }
|
||||||
|
};
|
||||||
|
const restage = async (id) => {
|
||||||
|
try { await poFetch('/items/' + id + '/stage', { method: 'POST' }); onReload(); }
|
||||||
|
catch (err) { setDropErr(err.message || 'Failed to restage'); }
|
||||||
|
};
|
||||||
|
|
||||||
|
const totalSecs = items.reduce((sum, it) => sum + itemEffectiveDuration(it), 0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="panel po-playlist" onDragOver={e => e.preventDefault()} onDrop={onContainerDrop}>
|
||||||
|
<div className="po-playlist-head">
|
||||||
|
<span className="po-section-label">Playlist</span>
|
||||||
|
{dropErr && <span className="po-drop-err">{dropErr}</span>}
|
||||||
|
</div>
|
||||||
|
{items.length === 0 && (
|
||||||
|
<div className="muted po-playlist-empty">Drag clips here to build the playlist.</div>
|
||||||
|
)}
|
||||||
|
{items.map((it, index) => {
|
||||||
|
const isActive = index === activeIndex;
|
||||||
|
const dur = itemEffectiveDuration(it);
|
||||||
|
return (
|
||||||
|
<div key={it.id}
|
||||||
|
className={'po-pl-item' + (isActive ? ' po-pl-item--active' : '')}
|
||||||
|
draggable
|
||||||
|
onDragStart={e => onItemDragStart(e, index)}
|
||||||
|
onDragOver={onItemDragOver}
|
||||||
|
onDrop={e => onItemDrop(e, index)}>
|
||||||
|
<span className="po-pl-index">
|
||||||
|
{isActive ? <span className="po-pl-onair">▶</span> : index + 1}
|
||||||
|
</span>
|
||||||
|
<span className="po-pl-name">{it.clip_name || it.asset_id}</span>
|
||||||
|
<span className="mono po-pl-dur">{fmtDuration(dur)}</span>
|
||||||
|
<span className={'badge po-pl-badge ' + (it.media_status === 'ready' ? 'success' : it.media_status === 'staging' ? 'warn' : it.media_status === 'error' ? 'error' : 'neutral')}>
|
||||||
|
{it.media_status}
|
||||||
|
</span>
|
||||||
|
{it.media_status === 'error' && (
|
||||||
|
<button className="btn ghost xs" onClick={() => restage(it.id)}>Retry</button>
|
||||||
|
)}
|
||||||
|
<button className="btn ghost xs" onClick={() => removeItem(it.id)}>✕</button>
|
||||||
|
<StagingBar status={it.media_status} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{items.length > 0 && (
|
||||||
|
<div className="po-playlist-footer">
|
||||||
|
<span className="mono muted">{items.length} clip{items.length !== 1 ? 's' : ''}</span>
|
||||||
|
<span className="mono po-pl-total">{fmtDuration(totalSecs)} total</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Transport bar ────────────────────────────────────────────────────────────
|
||||||
|
function Transport({ channel, playlistId, items, onStatus }) {
|
||||||
|
const [busy, setBusy] = React.useState(false);
|
||||||
|
const act = async (fn) => { setBusy(true); try { await fn(); } catch (e) { alert(e.message); } finally { setBusy(false); } };
|
||||||
|
|
||||||
|
const notReady = items.filter(i => i.media_status !== 'ready').length;
|
||||||
|
const canPlay = channel.status === 'running' && !busy && !!playlistId && notReady === 0;
|
||||||
|
|
||||||
|
const play = () => act(async () => {
|
||||||
|
const r = await poFetch('/channels/' + channel.id + '/play', {
|
||||||
|
method: 'POST', body: JSON.stringify({ playlist_id: playlistId }),
|
||||||
|
});
|
||||||
|
onStatus && onStatus(r);
|
||||||
|
});
|
||||||
|
const pause = () => act(() => poFetch('/channels/' + channel.id + '/pause', { method: 'POST' }));
|
||||||
|
const resume = () => act(() => poFetch('/channels/' + channel.id + '/resume', { method: 'POST' }));
|
||||||
|
const skip = () => act(() => poFetch('/channels/' + channel.id + '/skip', { method: 'POST' }));
|
||||||
|
const stopPb = () => act(() => poFetch('/channels/' + channel.id + '/stop-playback', { method: 'POST' }));
|
||||||
|
|
||||||
|
const live = channel.status === 'running';
|
||||||
|
return (
|
||||||
|
<div className="po-transport">
|
||||||
|
<button className="btn primary" disabled={!canPlay} onClick={play} title={notReady > 0 ? notReady + ' clip(s) still staging' : ''}>
|
||||||
|
{notReady > 0 && live ? '⏳ ' + notReady + ' staging' : '▶ Play'}
|
||||||
|
</button>
|
||||||
|
<button className="btn ghost" disabled={!live || busy} onClick={pause}>⏸ Pause</button>
|
||||||
|
<button className="btn ghost" disabled={!live || busy} onClick={resume}>⏵ Resume</button>
|
||||||
|
<button className="btn ghost" disabled={!live || busy} onClick={skip}>⏭ Skip</button>
|
||||||
|
<button className="btn danger ghost" disabled={!live || busy} onClick={stopPb}>⏹ Stop</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Elapsed timer ─────────────────────────────────────────────────────────────
|
||||||
|
function useElapsed(startedAt) {
|
||||||
|
const [elapsed, setElapsed] = React.useState(0);
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!startedAt) { setElapsed(0); return; }
|
||||||
|
const base = new Date(startedAt).getTime();
|
||||||
|
const tick = () => setElapsed(Math.max(0, Math.floor((Date.now() - base) / 1000)));
|
||||||
|
tick();
|
||||||
|
const id = setInterval(tick, 500);
|
||||||
|
return () => clearInterval(id);
|
||||||
|
}, [startedAt]);
|
||||||
|
return elapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtElapsed(secs) {
|
||||||
|
const h = Math.floor(secs / 3600);
|
||||||
|
const m = Math.floor((secs % 3600) / 60);
|
||||||
|
const s = secs % 60;
|
||||||
|
return (h > 0 ? String(h).padStart(2,'0') + ':' : '') +
|
||||||
|
String(m).padStart(2,'0') + ':' + String(s).padStart(2,'0');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Program monitor ──────────────────────────────────────────────────────────
|
||||||
|
function ProgramMonitor({ channel, engine }) {
|
||||||
|
const videoRef = React.useRef(null);
|
||||||
|
const hlsRef = React.useRef(null);
|
||||||
|
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 elapsed = useElapsed(engine && engine.currentItemStartedAt);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const vid = videoRef.current;
|
||||||
|
if (!vid) return;
|
||||||
|
|
||||||
|
// Tear down any previous HLS instance before re-evaluating.
|
||||||
|
if (hlsRef.current) { hlsRef.current.destroy(); hlsRef.current = null; }
|
||||||
|
|
||||||
|
if (!onAir) { vid.src = ''; return; }
|
||||||
|
|
||||||
|
if (window.Hls && window.Hls.isSupported()) {
|
||||||
|
const hls = new window.Hls({
|
||||||
|
liveSyncDurationCount: 3,
|
||||||
|
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; },
|
||||||
|
});
|
||||||
|
hlsRef.current = hls;
|
||||||
|
hls.loadSource(previewUrl);
|
||||||
|
hls.attachMedia(vid);
|
||||||
|
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 — which is
|
||||||
|
// exactly the "flashes a frame then stays black" symptom: hls.js renders
|
||||||
|
// a fragment or two, hits an unrecovered error, and never resumes. 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) {
|
||||||
|
// Non-fatal buffer stalls: nudge hls.js back to the live edge.
|
||||||
|
if (data.details === window.Hls.ErrorDetails.BUFFER_STALLED_ERROR) {
|
||||||
|
try { hls.startLoad(); } catch (_) {}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (data.type) {
|
||||||
|
case window.Hls.ErrorTypes.NETWORK_ERROR:
|
||||||
|
// Playlist/fragment load errors against the live edge are usually
|
||||||
|
// transient (segment rotated or mid-write). Re-arm the loader.
|
||||||
|
try { hls.startLoad(); } catch (_) {}
|
||||||
|
break;
|
||||||
|
case window.Hls.ErrorTypes.MEDIA_ERROR:
|
||||||
|
// Decode/buffer-append failures: flush + rebuild the buffer.
|
||||||
|
recoverCount += 1;
|
||||||
|
if (recoverCount <= 3) {
|
||||||
|
try { hls.recoverMediaError(); } catch (_) {}
|
||||||
|
} else {
|
||||||
|
// Repeated media errors: full reload of the source from scratch.
|
||||||
|
recoverCount = 0;
|
||||||
|
try { hls.destroy(); } catch (_) {}
|
||||||
|
if (hlsRef.current === hls) hlsRef.current = null;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Unrecoverable: drop the instance so a re-render can re-init.
|
||||||
|
try { hls.destroy(); } catch (_) {}
|
||||||
|
if (hlsRef.current === hls) hlsRef.current = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// A stalled <video> (readyState frozen) gets a gentle kick back to live.
|
||||||
|
hls.on(window.Hls.Events.FRAG_BUFFERED, () => {
|
||||||
|
if (vid.paused) vid.play().catch(() => {});
|
||||||
|
});
|
||||||
|
} else if (vid.canPlayType('application/vnd.apple.mpegurl')) {
|
||||||
|
// Native HLS (Safari).
|
||||||
|
vid.src = previewUrl;
|
||||||
|
vid.play().catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (hlsRef.current) { hlsRef.current.destroy(); hlsRef.current = null; }
|
||||||
|
};
|
||||||
|
}, [onAir, channel.id]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="po-monitor">
|
||||||
|
<div className="po-monitor-head">
|
||||||
|
<span className={'po-onair ' + (onAir ? 'live' : '')}>{onAir ? '● ON AIR' : '○ OFF'}</span>
|
||||||
|
<span className="mono muted">{channel.output_type?.toUpperCase()} · {channel.video_format}</span>
|
||||||
|
</div>
|
||||||
|
<div className="po-monitor-screen">
|
||||||
|
<video ref={videoRef} className="po-monitor-video" muted playsInline autoPlay />
|
||||||
|
{!onAir && (
|
||||||
|
<div className="po-monitor-overlay muted">Channel stopped</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="po-monitor-foot mono muted">
|
||||||
|
{engine && engine.currentClip
|
||||||
|
? <span className="po-monitor-clip-name">{engine.currentClip}</span>
|
||||||
|
: <span>{onAir ? 'Idle' : 'Stopped'}</span>}
|
||||||
|
{engine && engine.currentIndex >= 0 && (
|
||||||
|
<span style={{ marginLeft: 'auto', display: 'flex', gap: 10 }}>
|
||||||
|
<span style={{ color: 'var(--success)', fontVariantNumeric: 'tabular-nums' }}>
|
||||||
|
{fmtElapsed(elapsed)}
|
||||||
|
</span>
|
||||||
|
<span>clip {engine.currentIndex + 1}/{engine.playlistLength || 0}</span>
|
||||||
|
{engine.loop && <span>↺</span>}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{engine && engine.lastError && (
|
||||||
|
<span style={{ color: 'var(--warning)', fontSize: 10, marginLeft: 6 }} title={engine.lastError}>⚠</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Channel detail (monitors + bin + playlist + transport) ───────────────────
|
||||||
|
// As-run compliance log. Polls the existing GET /channels/:id/asrun endpoint
|
||||||
|
// (rows written by the scheduler health tick on every clip change) and shows the
|
||||||
|
// most recent plays: start time, clip, on-air duration, result.
|
||||||
|
function AsRunPanel({ channel, refreshKey }) {
|
||||||
|
const [rows, setRows] = React.useState([]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
let alive = true;
|
||||||
|
let t;
|
||||||
|
const poll = async () => {
|
||||||
|
try {
|
||||||
|
const r = await poFetch('/channels/' + channel.id + '/asrun');
|
||||||
|
if (alive) setRows(Array.isArray(r) ? r : []);
|
||||||
|
} catch (_) {}
|
||||||
|
t = setTimeout(poll, 5000);
|
||||||
|
};
|
||||||
|
poll();
|
||||||
|
return () => { alive = false; clearTimeout(t); };
|
||||||
|
}, [channel.id, refreshKey]);
|
||||||
|
|
||||||
|
const fmtTime = (ts) => {
|
||||||
|
if (!ts) return '—';
|
||||||
|
const d = new Date(ts);
|
||||||
|
return isNaN(d) ? '—' : d.toLocaleTimeString();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="po-asrun">
|
||||||
|
<div className="po-section-label">As-Run Log</div>
|
||||||
|
{rows.length === 0
|
||||||
|
? <div className="mono muted" style={{ padding: '8px 0' }}>No as-run entries yet.</div>
|
||||||
|
: (
|
||||||
|
<table className="po-asrun-table">
|
||||||
|
<thead>
|
||||||
|
<tr><th>Time</th><th>Clip</th><th>Duration</th><th>Result</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{rows.slice(0, 50).map((r) => (
|
||||||
|
<tr key={r.id}>
|
||||||
|
<td className="mono">{fmtTime(r.started_at)}</td>
|
||||||
|
<td>{r.clip_name || r.item_id || '—'}</td>
|
||||||
|
<td className="mono">{r.duration_s != null ? fmtDuration(Number(r.duration_s)) : (r.ended_at ? '—' : 'on air')}</td>
|
||||||
|
<td className={'po-asrun-result po-asrun-' + (r.result || 'played')}>{r.result || 'played'}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChannelDetail({ channel, onChannelChange }) {
|
||||||
|
const [playlists, setPlaylists] = React.useState([]);
|
||||||
|
const [playlistId, setPlaylistId] = React.useState(null);
|
||||||
|
const [items, setItems] = React.useState([]);
|
||||||
|
const [engine, setEngine] = React.useState(null);
|
||||||
|
const [ch, setCh] = React.useState(channel);
|
||||||
|
|
||||||
|
React.useEffect(() => { setCh(channel); }, [channel.id]);
|
||||||
|
|
||||||
|
const loadPlaylists = React.useCallback(async () => {
|
||||||
|
const pls = await poFetch('/playlists?channel_id=' + channel.id);
|
||||||
|
setPlaylists(pls);
|
||||||
|
if (pls.length && !playlistId) setPlaylistId(pls[0].id);
|
||||||
|
if (!pls.length) {
|
||||||
|
// Auto-create a default playlist so the operator can start dragging.
|
||||||
|
const created = await poFetch('/playlists', {
|
||||||
|
method: 'POST', body: JSON.stringify({ channel_id: channel.id, name: 'Main' }),
|
||||||
|
});
|
||||||
|
setPlaylists([created]); setPlaylistId(created.id);
|
||||||
|
}
|
||||||
|
}, [channel.id]);
|
||||||
|
|
||||||
|
const loadItems = React.useCallback(async () => {
|
||||||
|
if (!playlistId) return;
|
||||||
|
const its = await poFetch('/playlists/' + playlistId + '/items');
|
||||||
|
setItems(its);
|
||||||
|
}, [playlistId]);
|
||||||
|
|
||||||
|
React.useEffect(() => { loadPlaylists(); }, [channel.id]);
|
||||||
|
React.useEffect(() => { loadItems(); }, [playlistId]);
|
||||||
|
|
||||||
|
// Poll engine status + item staging while live.
|
||||||
|
React.useEffect(() => {
|
||||||
|
let t;
|
||||||
|
const poll = async () => {
|
||||||
|
try {
|
||||||
|
const s = await poFetch('/channels/' + channel.id + '/status');
|
||||||
|
setEngine(s.engine || null);
|
||||||
|
} catch (_) {}
|
||||||
|
try { await loadItems(); } catch (_) {}
|
||||||
|
t = setTimeout(poll, 4000);
|
||||||
|
};
|
||||||
|
poll();
|
||||||
|
return () => clearTimeout(t);
|
||||||
|
}, [channel.id, playlistId]);
|
||||||
|
|
||||||
|
const startChannel = async () => {
|
||||||
|
const updated = await poFetch('/channels/' + channel.id + '/start', { method: 'POST' });
|
||||||
|
setCh(updated); onChannelChange(updated);
|
||||||
|
};
|
||||||
|
const stopChannel = async () => {
|
||||||
|
const updated = await poFetch('/channels/' + channel.id + '/stop', { method: 'POST' });
|
||||||
|
setCh(updated); onChannelChange(updated);
|
||||||
|
};
|
||||||
|
const deleteChannel = async () => {
|
||||||
|
if (!window.confirm('Delete channel "' + ch.name + '"? This cannot be undone.')) return;
|
||||||
|
try {
|
||||||
|
await poFetch('/channels/' + ch.id, { method: 'DELETE' });
|
||||||
|
onChannelChange({ ...ch, _deleted: true });
|
||||||
|
} catch (e) { alert(e.message); }
|
||||||
|
};
|
||||||
|
|
||||||
|
// engine.currentIndex maps directly to the sorted item position.
|
||||||
|
const activeIndex = (engine && engine.currentIndex >= 0) ? engine.currentIndex : -1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="po-detail">
|
||||||
|
<div className="po-detail-head">
|
||||||
|
<div>
|
||||||
|
<h3 style={{ margin: 0 }}>{ch.name}</h3>
|
||||||
|
<span className="mono muted">{ch.output_type?.toUpperCase()} · {ch.video_format} · {ch.status}</span>
|
||||||
|
</div>
|
||||||
|
<div className="po-detail-actions">
|
||||||
|
{ch.status === 'running'
|
||||||
|
? <button className="btn danger" onClick={stopChannel}>Stop channel</button>
|
||||||
|
: <button className="btn primary" onClick={startChannel}>Start channel</button>}
|
||||||
|
{ch.status !== 'running' && (
|
||||||
|
<button className="btn ghost danger sm" onClick={deleteChannel} title="Delete this channel">Delete</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{ch.error_message && <div className="alert error">{ch.error_message}</div>}
|
||||||
|
|
||||||
|
<div className="po-grid">
|
||||||
|
<ProgramMonitor channel={ch} engine={engine} />
|
||||||
|
<MediaBin projectId={ch.project_id} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Transport channel={ch} playlistId={playlistId} items={items} onStatus={() => loadItems()} />
|
||||||
|
|
||||||
|
{playlistId && (
|
||||||
|
<Playlist
|
||||||
|
channel={ch}
|
||||||
|
playlistId={playlistId}
|
||||||
|
items={items}
|
||||||
|
activeIndex={activeIndex}
|
||||||
|
onReload={loadItems}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<AsRunPanel channel={ch} refreshKey={engine && engine.currentItemId} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Top-level page ───────────────────────────────────────────────────────────
|
||||||
|
function Playout() {
|
||||||
|
const [channels, setChannels] = React.useState(null);
|
||||||
|
const [selectedId, setSelectedId] = React.useState(null);
|
||||||
|
const [showCreate, setShowCreate] = React.useState(false);
|
||||||
|
const [err, setErr] = React.useState(null);
|
||||||
|
|
||||||
|
const load = React.useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const list = await poFetch('/channels');
|
||||||
|
setChannels(list);
|
||||||
|
if (list.length && !selectedId) setSelectedId(list[0].id);
|
||||||
|
} catch (e) { setErr(e.message); setChannels([]); }
|
||||||
|
}, [selectedId]);
|
||||||
|
|
||||||
|
React.useEffect(() => { load(); }, []);
|
||||||
|
|
||||||
|
const selected = (channels || []).find(c => c.id === selectedId) || null;
|
||||||
|
const onChannelChange = (updated) => {
|
||||||
|
if (updated._deleted) {
|
||||||
|
setChannels(cs => {
|
||||||
|
const next = (cs || []).filter(c => c.id !== updated.id);
|
||||||
|
setSelectedId(next.length ? next[0].id : null);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setChannels(cs => (cs || []).map(c => c.id === updated.id ? updated : c));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="page">
|
||||||
|
<div className="page-header">
|
||||||
|
<span className="title">Playout — Master Control</span>
|
||||||
|
<span className="subtitle">Schedule and play assets to SDI, NDI, SRT or RTMP.</span>
|
||||||
|
</div>
|
||||||
|
<div className="page-body po-page">
|
||||||
|
{err && <div className="alert error">{err}</div>}
|
||||||
|
<div className="po-channels-bar">
|
||||||
|
{(channels || []).map(c => (
|
||||||
|
<button key={c.id}
|
||||||
|
className={'po-chan-tab ' + (c.id === selectedId ? 'active' : '')}
|
||||||
|
onClick={() => setSelectedId(c.id)}>
|
||||||
|
<span className={'po-chan-dot ' + (c.status === 'running' ? 'live' : '')} />
|
||||||
|
{c.name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
<button className="btn ghost sm" onClick={() => setShowCreate(true)}>+ Channel</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{channels === null && <div className="muted">Loading channels…</div>}
|
||||||
|
{channels !== null && channels.length === 0 && (
|
||||||
|
<div className="po-empty">
|
||||||
|
<p className="muted">No playout channels yet.</p>
|
||||||
|
<button className="btn primary" onClick={() => setShowCreate(true)}>Create your first channel</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{selected && <ChannelDetail key={selected.id} channel={selected} onChannelChange={onChannelChange} />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showCreate && (
|
||||||
|
<ChannelCreate
|
||||||
|
onClose={() => setShowCreate(false)}
|
||||||
|
onCreated={(ch) => { setShowCreate(false); setChannels(cs => [...(cs || []), ch]); setSelectedId(ch.id); }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.Playout = Playout;
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue