capture: custom FFmpeg 7.1 build with DeckLink + D-Bus mounts + SDI deinterlace

Dockerfile is now a two-stage build that compiles FFmpeg from source with --enable-decklink against the Blackmagic SDK 16.x headers in services/capture/sdk/ (operator-supplied, gitignored). build-with-decklink.sh + patch_decklink.py drive the build.

docker-compose.yml mounts /dev/shm, /run/dbus, /run/systemd into mam-api, capture, web-ui so the BMD runtime can talk to the host.

capture-manager.js wraps SDI sources with -vf yadif=mode=1 (deinterlace).

recorders.html defaults to SDI source type now that we have a working DeckLink path.
This commit is contained in:
Zac Gaetano 2026-05-21 23:57:22 +00:00
parent 1074104d34
commit a8656fc1a8
7 changed files with 167 additions and 6 deletions

9
.gitignore vendored
View file

@ -23,3 +23,12 @@ services/editor/node_modules
services/editor/**/node_modules
services/editor/**/dist
services/editor/.pnpm-store
# Blackmagic DeckLink SDK + runtime libs (operator-supplied; see services/capture/build-with-decklink.sh)
services/capture/sdk/
services/capture/lib/
# Editor backups
*.bak
*.bak2
.env.bak.*

View file

@ -35,6 +35,9 @@ services:
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /mnt/NVME/MAM/wild-dragon-live:/live
- /dev/shm:/dev/shm
- /run/dbus:/run/dbus
- /run/systemd:/run/systemd
- /usr/bin/nvidia-smi:/usr/bin/nvidia-smi:ro
environment:
DATABASE_URL: ${DATABASE_URL}
@ -77,6 +80,9 @@ services:
MAM_API_URL: ${MAM_API_URL:-http://mam-api:3000}
volumes:
- /mnt/NVME/MAM/wild-dragon-live:/live
- /dev/shm:/dev/shm
- /run/dbus:/run/dbus
- /run/systemd:/run/systemd
networks:
- wild-dragon
@ -102,6 +108,9 @@ services:
- "${PORT_WEB_UI:-7434}:80"
volumes:
- /mnt/NVME/MAM/wild-dragon-live:/live
- /dev/shm:/dev/shm
- /run/dbus:/run/dbus
- /run/systemd:/run/systemd
networks:
- wild-dragon

View file

@ -1,8 +1,66 @@
# ── Stage 1: Build FFmpeg with DeckLink support ─────────────────────────────
FROM debian:bookworm AS ffmpeg-builder
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential nasm yasm pkg-config git ca-certificates python3 \
libssl-dev libx264-dev libx265-dev libvpx-dev libopus-dev \
libmp3lame-dev libsrt-openssl-dev \
libzmq3-dev zlib1g-dev libstdc++-12-dev \
&& rm -rf /var/lib/apt/lists/*
# Copy in BMD DeckLink SDK headers and patch script
COPY sdk/ /decklink-sdk/
COPY patch_decklink.py /patch_decklink.py
# Pull FFmpeg 7.1 source
RUN git clone --depth=1 --branch release/7.1 https://git.ffmpeg.org/ffmpeg.git /ffmpeg
# Patch FFmpeg DeckLink code for SDK 16.x API changes
RUN python3 /patch_decklink.py
WORKDIR /ffmpeg
RUN ./configure \
--prefix=/usr/local \
--enable-gpl \
--enable-nonfree \
--enable-libx264 \
--enable-libx265 \
--enable-libvpx \
--enable-libopus \
--enable-libmp3lame \
--enable-libsrt \
--enable-libzmq \
--enable-decklink \
--extra-cflags="-I/decklink-sdk" \
--disable-doc \
--disable-debug \
--disable-ffplay \
&& make -j$(nproc) \
&& make install
# ── Stage 2: Runtime image ───────────────────────────────────────────────────
FROM node:20-bookworm
RUN apt-get update && apt-get install -y ffmpeg && rm -rf /var/lib/apt/lists/*
# Runtime deps for compiled ffmpeg libs
RUN apt-get update && apt-get install -y --no-install-recommends \
libx264-164 libx265-199 libvpx7 libopus0 libmp3lame0 \
libsrt1.5-openssl libzmq5 libstdc++6 libc++1 libc++abi1 \
&& rm -rf /var/lib/apt/lists/*
# Copy compiled ffmpeg/ffprobe
COPY --from=ffmpeg-builder /usr/local/bin/ffmpeg /usr/local/bin/ffmpeg
COPY --from=ffmpeg-builder /usr/local/bin/ffprobe /usr/local/bin/ffprobe
COPY --from=ffmpeg-builder /usr/local/lib/ /usr/local/lib/
# DeckLink runtime .so
COPY lib/libDeckLinkAPI.so /usr/lib/libDeckLinkAPI.so
COPY lib/libDeckLinkPreviewAPI.so /usr/lib/libDeckLinkPreviewAPI.so
RUN ldconfig
WORKDIR /app
COPY package*.json ./
RUN npm install --omit=dev
COPY . .
EXPOSE 3001
CMD ["node", "src/index.js"]

View file

@ -0,0 +1,30 @@
#!/bin/bash
set -e
cd "$(dirname "$0")"
echo "=== Checking prerequisites ==="
if [ ! -f sdk/DeckLinkAPI.h ]; then
echo "ERROR: sdk/DeckLinkAPI.h not found."
echo ""
echo "Please download the Blackmagic DeckLink SDK 16.x from:"
echo " https://www.blackmagicdesign.com/developer/product/capture"
echo ""
echo "Then extract the Linux/include/ folder contents into:"
echo " $(pwd)/sdk/"
echo ""
echo "Required files: DeckLinkAPI.h DeckLinkAPIVersion.h DeckLinkAPIDispatch.cpp"
echo " LinuxCOM.h DeckLinkAPIModes.h DeckLinkAPITypes.h"
exit 1
fi
echo "SDK headers found:"
ls sdk/*.h sdk/*.cpp 2>/dev/null
echo ""
echo "=== Building capture container with DeckLink FFmpeg ==="
docker compose -f ../../docker-compose.yml build capture
echo ""
echo "=== Verifying DeckLink support in built image ==="
docker run --rm wild-dragon-capture ffmpeg -f decklink -list_devices true -i dummy 2>&1 | head -20

View file

@ -0,0 +1,51 @@
import re
dec_path = '/ffmpeg/libavdevice/decklink_dec.cpp'
with open(dec_path) as f:
content = f.read()
# Fix 1: IDeckLinkMemoryAllocator -> IDeckLinkMemoryAllocator_v14_2_1
# SDK 16 removed the unversioned alias
for old, new in [
(': public IDeckLinkMemoryAllocator\n', ': public IDeckLinkMemoryAllocator_v14_2_1\n'),
('IDeckLinkMemoryAllocator *', 'IDeckLinkMemoryAllocator_v14_2_1 *'),
('IDeckLinkMemoryAllocator*', 'IDeckLinkMemoryAllocator_v14_2_1*'),
]:
content = content.replace(old, new)
print('Fix 1: IDeckLinkMemoryAllocator renamed')
# Fix 2: SetVideoInputFrameMemoryAllocator removed from IDeckLinkInput in SDK 16
content = re.sub(
r'ret = \(ctx->dli->SetVideoInputFrameMemoryAllocator\(allocator\)[^;]*;',
'ret = 0; /* SDK16: SetVideoInputFrameMemoryAllocator removed */',
content
)
print('Fix 2: SetVideoInputFrameMemoryAllocator patched')
# Fix 3: IDeckLinkVideoFrame::GetBytes removed in SDK 16 - moved to IDeckLinkVideoBuffer
# Replace: videoFrame->GetBytes(&frameBytes);
# With: { IDeckLinkVideoBuffer *vbuf = nullptr; videoFrame->QueryInterface(IID_IDeckLinkVideoBuffer, (void**)&vbuf); if (vbuf) { vbuf->GetBytes(&frameBytes); vbuf->Release(); } }
getbytes_replacement = (
'{ IDeckLinkVideoBuffer *_vbuf = nullptr; '
'videoFrame->QueryInterface(IID_IDeckLinkVideoBuffer, (void**)&_vbuf); '
'if (_vbuf) { _vbuf->GetBytes(&frameBytes); _vbuf->Release(); } }'
)
content = content.replace(
'videoFrame->GetBytes(&frameBytes);',
getbytes_replacement + ';'
)
print('Fix 3: videoFrame->GetBytes replaced with QueryInterface(IDeckLinkVideoBuffer)')
# Fix 4: Add include for versioned allocator header
if 'DeckLinkAPIMemoryAllocator_v14_2_1.h' not in content:
content = content.replace(
'#include "decklink_common.h"',
'#include "decklink_common.h"\n#include "DeckLinkAPIMemoryAllocator_v14_2_1.h"'
)
print('Fix 4: DeckLinkAPIMemoryAllocator_v14_2_1.h include added')
with open(dec_path, 'w') as f:
f.write(content)
print('All patches applied successfully')

View file

@ -213,8 +213,11 @@ class CaptureManager {
console.log('[capture] hires ffmpeg args:', hiresCodecArgs.join(' '));
const sdiFilterArgs = (sourceType === 'sdi') ? ['-vf', 'yadif=mode=1'] : [];
const hiresProcess = spawn('ffmpeg', [
...inputArgs,
...sdiFilterArgs,
...hiresCodecArgs,
'pipe:1',
], { stdio: ['ignore', 'pipe', 'pipe'] });
@ -286,6 +289,7 @@ class CaptureManager {
const proxyProcess = spawn('ffmpeg', [
...inputArgs,
...sdiFilterArgs,
...proxyCodecArgs,
'-movflags', '+frag_keyframe+empty_moov',
'pipe:1',

View file

@ -386,9 +386,9 @@
<div class="form-group">
<label class="form-label">Source type</label>
<div class="source-type-row">
<button class="source-type-btn active" data-type="srt" onclick="setSourceType('srt')">SRT</button>
<button class="source-type-btn" data-type="srt" onclick="setSourceType('srt')">SRT</button>
<button class="source-type-btn" data-type="rtmp" onclick="setSourceType('rtmp')">RTMP</button>
<button class="source-type-btn" data-type="sdi" onclick="setSourceType('sdi')">SDI</button>
<button class="source-type-btn active" data-type="sdi" onclick="setSourceType('sdi')">SDI</button>
</div>
</div>
@ -662,7 +662,7 @@
const PROXY_DEFAULT_CONTAINER = 'mp4';
const pState = {
recorders: [], timers: {}, sourceType: 'srt', mode: 'caller',
recorders: [], timers: {}, sourceType: 'sdi', mode: 'caller',
projects: [], signals: {}, editingId: null,
// SDI picker state
bmdDevices: [], // flat list from /cluster/devices/blackmagic
@ -1068,10 +1068,10 @@
const pr = document.getElementById('probeResult');
if (pr) pr.remove();
pState.sourceType = 'srt';
pState.sourceType = 'sdi';
pState.selectedNodeId = null;
pState.selectedDeviceIndex = null;
document.querySelectorAll('.source-type-btn').forEach(b => b.classList.toggle('active', b.dataset.type === 'srt'));
document.querySelectorAll('.source-type-btn').forEach(b => b.classList.toggle('active', b.dataset.type === 'sdi'));
// Make sure tabs are on Video
document.querySelectorAll('.codec-tabs').forEach(tabs => {
tabs.querySelectorAll('.codec-tab').forEach(b => b.classList.toggle('active', b.dataset.tab === 'video'));