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:
parent
1074104d34
commit
a8656fc1a8
7 changed files with 167 additions and 6 deletions
9
.gitignore
vendored
9
.gitignore
vendored
|
|
@ -23,3 +23,12 @@ services/editor/node_modules
|
||||||
services/editor/**/node_modules
|
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)
|
||||||
|
services/capture/sdk/
|
||||||
|
services/capture/lib/
|
||||||
|
|
||||||
|
# Editor backups
|
||||||
|
*.bak
|
||||||
|
*.bak2
|
||||||
|
.env.bak.*
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,9 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
- /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
|
||||||
|
- /dev/shm:/dev/shm
|
||||||
|
- /run/dbus:/run/dbus
|
||||||
|
- /run/systemd:/run/systemd
|
||||||
- /usr/bin/nvidia-smi:/usr/bin/nvidia-smi:ro
|
- /usr/bin/nvidia-smi:/usr/bin/nvidia-smi:ro
|
||||||
environment:
|
environment:
|
||||||
DATABASE_URL: ${DATABASE_URL}
|
DATABASE_URL: ${DATABASE_URL}
|
||||||
|
|
@ -77,6 +80,9 @@ services:
|
||||||
MAM_API_URL: ${MAM_API_URL:-http://mam-api:3000}
|
MAM_API_URL: ${MAM_API_URL:-http://mam-api:3000}
|
||||||
volumes:
|
volumes:
|
||||||
- /mnt/NVME/MAM/wild-dragon-live:/live
|
- /mnt/NVME/MAM/wild-dragon-live:/live
|
||||||
|
- /dev/shm:/dev/shm
|
||||||
|
- /run/dbus:/run/dbus
|
||||||
|
- /run/systemd:/run/systemd
|
||||||
networks:
|
networks:
|
||||||
- wild-dragon
|
- wild-dragon
|
||||||
|
|
||||||
|
|
@ -102,6 +108,9 @@ 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
|
||||||
|
- /dev/shm:/dev/shm
|
||||||
|
- /run/dbus:/run/dbus
|
||||||
|
- /run/systemd:/run/systemd
|
||||||
networks:
|
networks:
|
||||||
- wild-dragon
|
- wild-dragon
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
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
|
WORKDIR /app
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
RUN npm install --omit=dev
|
RUN npm install --omit=dev
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
EXPOSE 3001
|
EXPOSE 3001
|
||||||
CMD ["node", "src/index.js"]
|
CMD ["node", "src/index.js"]
|
||||||
|
|
|
||||||
30
services/capture/build-with-decklink.sh
Executable file
30
services/capture/build-with-decklink.sh
Executable 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
|
||||||
51
services/capture/patch_decklink.py
Normal file
51
services/capture/patch_decklink.py
Normal 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')
|
||||||
|
|
@ -213,8 +213,11 @@ class CaptureManager {
|
||||||
|
|
||||||
console.log('[capture] hires ffmpeg args:', hiresCodecArgs.join(' '));
|
console.log('[capture] hires ffmpeg args:', hiresCodecArgs.join(' '));
|
||||||
|
|
||||||
|
const sdiFilterArgs = (sourceType === 'sdi') ? ['-vf', 'yadif=mode=1'] : [];
|
||||||
|
|
||||||
const hiresProcess = spawn('ffmpeg', [
|
const hiresProcess = spawn('ffmpeg', [
|
||||||
...inputArgs,
|
...inputArgs,
|
||||||
|
...sdiFilterArgs,
|
||||||
...hiresCodecArgs,
|
...hiresCodecArgs,
|
||||||
'pipe:1',
|
'pipe:1',
|
||||||
], { stdio: ['ignore', 'pipe', 'pipe'] });
|
], { stdio: ['ignore', 'pipe', 'pipe'] });
|
||||||
|
|
@ -286,6 +289,7 @@ class CaptureManager {
|
||||||
|
|
||||||
const proxyProcess = spawn('ffmpeg', [
|
const proxyProcess = spawn('ffmpeg', [
|
||||||
...inputArgs,
|
...inputArgs,
|
||||||
|
...sdiFilterArgs,
|
||||||
...proxyCodecArgs,
|
...proxyCodecArgs,
|
||||||
'-movflags', '+frag_keyframe+empty_moov',
|
'-movflags', '+frag_keyframe+empty_moov',
|
||||||
'pipe:1',
|
'pipe:1',
|
||||||
|
|
|
||||||
|
|
@ -386,9 +386,9 @@
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Source type</label>
|
<label class="form-label">Source type</label>
|
||||||
<div class="source-type-row">
|
<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="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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -662,7 +662,7 @@
|
||||||
const PROXY_DEFAULT_CONTAINER = 'mp4';
|
const PROXY_DEFAULT_CONTAINER = 'mp4';
|
||||||
|
|
||||||
const pState = {
|
const pState = {
|
||||||
recorders: [], timers: {}, sourceType: 'srt', mode: 'caller',
|
recorders: [], timers: {}, sourceType: 'sdi', mode: 'caller',
|
||||||
projects: [], signals: {}, editingId: null,
|
projects: [], signals: {}, editingId: null,
|
||||||
// SDI picker state
|
// SDI picker state
|
||||||
bmdDevices: [], // flat list from /cluster/devices/blackmagic
|
bmdDevices: [], // flat list from /cluster/devices/blackmagic
|
||||||
|
|
@ -1068,10 +1068,10 @@
|
||||||
const pr = document.getElementById('probeResult');
|
const pr = document.getElementById('probeResult');
|
||||||
if (pr) pr.remove();
|
if (pr) pr.remove();
|
||||||
|
|
||||||
pState.sourceType = 'srt';
|
pState.sourceType = 'sdi';
|
||||||
pState.selectedNodeId = null;
|
pState.selectedNodeId = null;
|
||||||
pState.selectedDeviceIndex = 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
|
// Make sure tabs are on Video
|
||||||
document.querySelectorAll('.codec-tabs').forEach(tabs => {
|
document.querySelectorAll('.codec-tabs').forEach(tabs => {
|
||||||
tabs.querySelectorAll('.codec-tab').forEach(b => b.classList.toggle('active', b.dataset.tab === 'video'));
|
tabs.querySelectorAll('.codec-tab').forEach(b => b.classList.toggle('active', b.dataset.tab === 'video'));
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue