diff --git a/.gitignore b/.gitignore index dae31b3..ec9d060 100644 --- a/.gitignore +++ b/.gitignore @@ -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.* diff --git a/docker-compose.yml b/docker-compose.yml index 9e324bb..0fdd44d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/services/capture/Dockerfile b/services/capture/Dockerfile index 9a8a226..f7fc2ac 100644 --- a/services/capture/Dockerfile +++ b/services/capture/Dockerfile @@ -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"] diff --git a/services/capture/build-with-decklink.sh b/services/capture/build-with-decklink.sh new file mode 100755 index 0000000..ed3f6c2 --- /dev/null +++ b/services/capture/build-with-decklink.sh @@ -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 diff --git a/services/capture/patch_decklink.py b/services/capture/patch_decklink.py new file mode 100644 index 0000000..c032227 --- /dev/null +++ b/services/capture/patch_decklink.py @@ -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') diff --git a/services/capture/src/capture-manager.js b/services/capture/src/capture-manager.js index d5f67ba..44b975f 100644 --- a/services/capture/src/capture-manager.js +++ b/services/capture/src/capture-manager.js @@ -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', diff --git a/services/web-ui/public/recorders.html b/services/web-ui/public/recorders.html index 3398837..45b4898 100644 --- a/services/web-ui/public/recorders.html +++ b/services/web-ui/public/recorders.html @@ -386,9 +386,9 @@