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/**/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.*
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
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(' '));
|
||||
|
||||
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',
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
|
|
|
|||
Loading…
Reference in a new issue