From 30b4deffc6dd0a2c94482932f5bccf1beb3a80dd Mon Sep 17 00:00:00 2001 From: Zac Gaetano Date: Fri, 22 May 2026 00:53:03 +0000 Subject: [PATCH] fix(capture): proper SDK 16 patch via upstream FFmpeg master diff The previous patch_decklink.py mixed v14_2_1 versioned types (Fix 1 renamed the allocator class) with no-ops for SetVideoInputFrameMemoryAllocator + QueryInterface-around-GetBytes (Fixes 2 & 3). That inconsistency compiled but silently dropped every video frame: VideoInputFrameArrived saw _v14_2_1 allocator output but tried to read it via the SDK 16 unversioned IDeckLinkVideoBuffer path, and the SDK released the buffer before FFmpeg could consume it. Bisected with the BMD-provided Capture sample at SDK 16 mode 5 (Hp29) which got frames cleanly, confirming the signal was fine and the bug was in FFmpegs decklink demuxer. Fix: pull libavdevice/decklink_{enc,dec,common}{.cpp,.h} from upstream FFmpeg master (commits past 7.1 that fully rename every decklink interface to its _v14_2_1 versioned form) and apply that diff in reverse during build. Now build is internally consistent and frames flow. Verified: SDI1 recorder on zampp2 hits 423 frames in 14s @ 29 fps, ProRes HQ at 91 Mbps. --- services/capture/Dockerfile | 1 + services/capture/decklink-sdk16.patch | 346 ++++++++++++++++++++++++++ services/capture/patch_decklink.py | 70 ++---- 3 files changed, 367 insertions(+), 50 deletions(-) create mode 100644 services/capture/decklink-sdk16.patch diff --git a/services/capture/Dockerfile b/services/capture/Dockerfile index f7fc2ac..daec5df 100644 --- a/services/capture/Dockerfile +++ b/services/capture/Dockerfile @@ -11,6 +11,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ # Copy in BMD DeckLink SDK headers and patch script COPY sdk/ /decklink-sdk/ COPY patch_decklink.py /patch_decklink.py +COPY decklink-sdk16.patch /decklink-sdk16.patch # Pull FFmpeg 7.1 source RUN git clone --depth=1 --branch release/7.1 https://git.ffmpeg.org/ffmpeg.git /ffmpeg diff --git a/services/capture/decklink-sdk16.patch b/services/capture/decklink-sdk16.patch new file mode 100644 index 0000000..7140f3f --- /dev/null +++ b/services/capture/decklink-sdk16.patch @@ -0,0 +1,346 @@ +diff --git a/libavdevice/decklink_common.cpp b/libavdevice/decklink_common.cpp +index fe187cd..47de7ef 100644 +--- a/libavdevice/decklink_common.cpp ++++ b/libavdevice/decklink_common.cpp +@@ -25,12 +25,7 @@ extern "C" { + #include "libavformat/internal.h" + } + +-#include + #include +-#if BLACKMAGIC_DECKLINK_API_VERSION >= 0x0e030000 +-#include +-#endif +- + #ifdef _WIN32 + #include + #else +@@ -517,8 +512,8 @@ int ff_decklink_list_devices(AVFormatContext *avctx, + return AVERROR(EIO); + + while (ret == 0 && iter->Next(&dl) == S_OK) { +- IDeckLinkOutput_v14_2_1 *output_config; +- IDeckLinkInput_v14_2_1 *input_config; ++ IDeckLinkOutput *output_config; ++ IDeckLinkInput *input_config; + const char *display_name = NULL; + const char *unique_name = NULL; + AVDeviceInfo *new_device = NULL; +@@ -532,14 +527,14 @@ int ff_decklink_list_devices(AVFormatContext *avctx, + goto next; + + if (show_outputs) { +- if (dl->QueryInterface(IID_IDeckLinkOutput_v14_2_1, (void **)&output_config) == S_OK) { ++ if (dl->QueryInterface(IID_IDeckLinkOutput, (void **)&output_config) == S_OK) { + output_config->Release(); + add = 1; + } + } + + if (show_inputs) { +- if (dl->QueryInterface(IID_IDeckLinkInput_v14_2_1, (void **)&input_config) == S_OK) { ++ if (dl->QueryInterface(IID_IDeckLinkInput, (void **)&input_config) == S_OK) { + input_config->Release(); + add = 1; + } +diff --git a/libavdevice/decklink_common.h b/libavdevice/decklink_common.h +index 095b438..6b32dc2 100644 +--- a/libavdevice/decklink_common.h ++++ b/libavdevice/decklink_common.h +@@ -29,23 +29,6 @@ + #define IDeckLinkProfileAttributes IDeckLinkAttributes + #endif + +-#if BLACKMAGIC_DECKLINK_API_VERSION < 0x0e030000 +-#define IDeckLinkInput_v14_2_1 IDeckLinkInput +-#define IDeckLinkInputCallback_v14_2_1 IDeckLinkInputCallback +-#define IDeckLinkMemoryAllocator_v14_2_1 IDeckLinkMemoryAllocator +-#define IDeckLinkOutput_v14_2_1 IDeckLinkOutput +-#define IDeckLinkVideoFrame_v14_2_1 IDeckLinkVideoFrame +-#define IDeckLinkVideoInputFrame_v14_2_1 IDeckLinkVideoInputFrame +-#define IDeckLinkVideoOutputCallback_v14_2_1 IDeckLinkVideoOutputCallback +-#define IID_IDeckLinkInput_v14_2_1 IID_IDeckLinkInput +-#define IID_IDeckLinkInputCallback_v14_2_1 IID_IDeckLinkInputCallback +-#define IID_IDeckLinkMemoryAllocator_v14_2_1 IID_IDeckLinkMemoryAllocator +-#define IID_IDeckLinkOutput_v14_2_1 IID_IDeckLinkOutput +-#define IID_IDeckLinkVideoFrame_v14_2_1 IID_IDeckLinkVideoFrame +-#define IID_IDeckLinkVideoInputFrame_v14_2_1 IID_IDeckLinkVideoInputFrame +-#define IID_IDeckLinkVideoOutputCallback_v14_2_1 IID_IDeckLinkVideoOutputCallback +-#endif +- + extern "C" { + #include "libavutil/mem.h" + #include "libavcodec/packet_internal.h" +@@ -93,16 +76,6 @@ static char *dup_cfstring_to_utf8(CFStringRef w) + #define DECKLINK_FREE(s) free((void *) s) + #endif + +-#ifdef _WIN32 +-#include // REFIID, IsEqualIID() +-#define DECKLINK_IsEqualIID IsEqualIID +-#else +-static inline bool DECKLINK_IsEqualIID(const REFIID& riid1, const REFIID& riid2) +-{ +- return memcmp(&riid1, &riid2, sizeof(REFIID)) == 0; +-} +-#endif +- + class decklink_output_callback; + class decklink_input_callback; + +@@ -120,8 +93,8 @@ typedef struct DecklinkPacketQueue { + struct decklink_ctx { + /* DeckLink SDK interfaces */ + IDeckLink *dl; +- IDeckLinkOutput_v14_2_1 *dlo; +- IDeckLinkInput_v14_2_1 *dli; ++ IDeckLinkOutput *dlo; ++ IDeckLinkInput *dli; + IDeckLinkConfiguration *cfg; + IDeckLinkProfileAttributes *attr; + decklink_output_callback *output_callback; +diff --git a/libavdevice/decklink_dec.cpp b/libavdevice/decklink_dec.cpp +index 8830779..418701e 100644 +--- a/libavdevice/decklink_dec.cpp ++++ b/libavdevice/decklink_dec.cpp +@@ -31,11 +31,7 @@ extern "C" { + #include "libavformat/internal.h" + } + +-#include + #include +-#if BLACKMAGIC_DECKLINK_API_VERSION >= 0x0e030000 +-#include +-#endif + + extern "C" { + #include "config.h" +@@ -109,7 +105,7 @@ static VANCLineNumber vanc_line_numbers[] = { + {bmdModeUnknown, 0, -1, -1, -1} + }; + +-class decklink_allocator : public IDeckLinkMemoryAllocator_v14_2_1 ++class decklink_allocator : public IDeckLinkMemoryAllocator + { + public: + decklink_allocator(): _refs(1) { } +@@ -133,21 +129,7 @@ public: + virtual HRESULT STDMETHODCALLTYPE Decommit() { return S_OK; } + + // IUnknown methods +- virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, LPVOID *ppv) +- { +- if (DECKLINK_IsEqualIID(riid, IID_IUnknown)) { +- *ppv = static_cast(this); +- } else if (DECKLINK_IsEqualIID(riid, IID_IDeckLinkMemoryAllocator_v14_2_1)) { +- *ppv = static_cast(this); +- } else { +- *ppv = NULL; +- return E_NOINTERFACE; +- } +- +- AddRef(); +- return S_OK; +- } +- ++ virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, LPVOID *ppv) { return E_NOINTERFACE; } + virtual ULONG STDMETHODCALLTYPE AddRef(void) { return ++_refs; } + virtual ULONG STDMETHODCALLTYPE Release(void) + { +@@ -490,7 +472,7 @@ skip_packet: + } + + +-static void handle_klv(AVFormatContext *avctx, decklink_ctx *ctx, IDeckLinkVideoInputFrame_v14_2_1 *videoFrame, int64_t pts) ++static void handle_klv(AVFormatContext *avctx, decklink_ctx *ctx, IDeckLinkVideoInputFrame *videoFrame, int64_t pts) + { + const uint8_t KLV_DID = 0x44; + const uint8_t KLV_IN_VANC_SDID = 0x04; +@@ -592,30 +574,17 @@ static void handle_klv(AVFormatContext *avctx, decklink_ctx *ctx, IDeckLinkVideo + } + } + +-class decklink_input_callback : public IDeckLinkInputCallback_v14_2_1 ++class decklink_input_callback : public IDeckLinkInputCallback + { + public: + explicit decklink_input_callback(AVFormatContext *_avctx); + ~decklink_input_callback(); + +- virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, LPVOID *ppv) +- { +- if (DECKLINK_IsEqualIID(riid, IID_IUnknown)) { +- *ppv = static_cast(this); +- } else if (DECKLINK_IsEqualIID(riid, IID_IDeckLinkInputCallback_v14_2_1)) { +- *ppv = static_cast(this); +- } else { +- *ppv = NULL; +- return E_NOINTERFACE; +- } +- +- AddRef(); +- return S_OK; +- } ++ virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, LPVOID *ppv) { return E_NOINTERFACE; } + virtual ULONG STDMETHODCALLTYPE AddRef(void); + virtual ULONG STDMETHODCALLTYPE Release(void); + virtual HRESULT STDMETHODCALLTYPE VideoInputFormatChanged(BMDVideoInputFormatChangedEvents, IDeckLinkDisplayMode*, BMDDetectedVideoInputFormatFlags); +- virtual HRESULT STDMETHODCALLTYPE VideoInputFrameArrived(IDeckLinkVideoInputFrame_v14_2_1*, IDeckLinkAudioInputPacket*); ++ virtual HRESULT STDMETHODCALLTYPE VideoInputFrameArrived(IDeckLinkVideoInputFrame*, IDeckLinkAudioInputPacket*); + + private: + std::atomic _refs; +@@ -624,7 +593,7 @@ private: + int no_video; + int64_t initial_video_pts; + int64_t initial_audio_pts; +- IDeckLinkVideoInputFrame_v14_2_1* last_video_frame; ++ IDeckLinkVideoInputFrame* last_video_frame; + }; + + decklink_input_callback::decklink_input_callback(AVFormatContext *_avctx) : _refs(1) +@@ -656,7 +625,7 @@ ULONG decklink_input_callback::Release(void) + return ret; + } + +-static int64_t get_pkt_pts(IDeckLinkVideoInputFrame_v14_2_1 *videoFrame, ++static int64_t get_pkt_pts(IDeckLinkVideoInputFrame *videoFrame, + IDeckLinkAudioInputPacket *audioFrame, + int64_t wallclock, + int64_t abs_wallclock, +@@ -710,7 +679,7 @@ static int64_t get_pkt_pts(IDeckLinkVideoInputFrame_v14_2_1 *videoFrame, + return pts; + } + +-static int get_bmd_timecode(AVFormatContext *avctx, AVTimecode *tc, AVRational frame_rate, BMDTimecodeFormat tc_format, IDeckLinkVideoInputFrame_v14_2_1 *videoFrame) ++static int get_bmd_timecode(AVFormatContext *avctx, AVTimecode *tc, AVRational frame_rate, BMDTimecodeFormat tc_format, IDeckLinkVideoInputFrame *videoFrame) + { + IDeckLinkTimecode *timecode; + int ret = AVERROR(ENOENT); +@@ -732,7 +701,7 @@ static int get_bmd_timecode(AVFormatContext *avctx, AVTimecode *tc, AVRational f + return ret; + } + +-static int get_frame_timecode(AVFormatContext *avctx, decklink_ctx *ctx, AVTimecode *tc, IDeckLinkVideoInputFrame_v14_2_1 *videoFrame) ++static int get_frame_timecode(AVFormatContext *avctx, decklink_ctx *ctx, AVTimecode *tc, IDeckLinkVideoInputFrame *videoFrame) + { + AVRational frame_rate = ctx->video_st->r_frame_rate; + int ret; +@@ -757,7 +726,7 @@ static int get_frame_timecode(AVFormatContext *avctx, decklink_ctx *ctx, AVTimec + } + + HRESULT decklink_input_callback::VideoInputFrameArrived( +- IDeckLinkVideoInputFrame_v14_2_1 *videoFrame, IDeckLinkAudioInputPacket *audioFrame) ++ IDeckLinkVideoInputFrame *videoFrame, IDeckLinkAudioInputPacket *audioFrame) + { + void *frameBytes; + void *audioFrameBytes; +@@ -1172,7 +1141,7 @@ av_cold int ff_decklink_read_header(AVFormatContext *avctx) + goto error; + + /* Get input device. */ +- if (ctx->dl->QueryInterface(IID_IDeckLinkInput_v14_2_1, (void **) &ctx->dli) != S_OK) { ++ if (ctx->dl->QueryInterface(IID_IDeckLinkInput, (void **) &ctx->dli) != S_OK) { + av_log(avctx, AV_LOG_ERROR, "Could not open input device from '%s'\n", + avctx->url); + ret = AVERROR(EIO); +diff --git a/libavdevice/decklink_enc.cpp b/libavdevice/decklink_enc.cpp +index d2e246c..cb8f917 100644 +--- a/libavdevice/decklink_enc.cpp ++++ b/libavdevice/decklink_enc.cpp +@@ -28,11 +28,7 @@ extern "C" { + #include "libavformat/internal.h" + } + +-#include + #include +-#if BLACKMAGIC_DECKLINK_API_VERSION >= 0x0e030000 +-#include +-#endif + + extern "C" { + #include "libavformat/avformat.h" +@@ -52,7 +48,7 @@ extern "C" { + #endif + + /* DeckLink callback class declaration */ +-class decklink_frame : public IDeckLinkVideoFrame_v14_2_1 ++class decklink_frame : public IDeckLinkVideoFrame + { + public: + decklink_frame(struct decklink_ctx *ctx, AVFrame *avframe, AVCodecID codec_id, int height, int width) : +@@ -115,20 +111,7 @@ public: + _ancillary->AddRef(); + return S_OK; + } +- virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, LPVOID *ppv) +- { +- if (DECKLINK_IsEqualIID(riid, IID_IUnknown)) { +- *ppv = static_cast(this); +- } else if (DECKLINK_IsEqualIID(riid, IID_IDeckLinkVideoFrame_v14_2_1)) { +- *ppv = static_cast(this); +- } else { +- *ppv = NULL; +- return E_NOINTERFACE; +- } +- +- AddRef(); +- return S_OK; +- } ++ virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, LPVOID *ppv) { return E_NOINTERFACE; } + virtual ULONG STDMETHODCALLTYPE AddRef(void) { return ++_refs; } + virtual ULONG STDMETHODCALLTYPE Release(void) + { +@@ -155,10 +138,10 @@ private: + std::atomic _refs; + }; + +-class decklink_output_callback : public IDeckLinkVideoOutputCallback_v14_2_1 ++class decklink_output_callback : public IDeckLinkVideoOutputCallback + { + public: +- virtual HRESULT STDMETHODCALLTYPE ScheduledFrameCompleted(IDeckLinkVideoFrame_v14_2_1 *_frame, BMDOutputFrameCompletionResult result) ++ virtual HRESULT STDMETHODCALLTYPE ScheduledFrameCompleted(IDeckLinkVideoFrame *_frame, BMDOutputFrameCompletionResult result) + { + decklink_frame *frame = static_cast(_frame); + struct decklink_ctx *ctx = frame->_ctx; +@@ -176,20 +159,7 @@ public: + return S_OK; + } + virtual HRESULT STDMETHODCALLTYPE ScheduledPlaybackHasStopped(void) { return S_OK; } +- virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, LPVOID *ppv) +- { +- if (DECKLINK_IsEqualIID(riid, IID_IUnknown)) { +- *ppv = static_cast(this); +- } else if (DECKLINK_IsEqualIID(riid, IID_IDeckLinkVideoOutputCallback_v14_2_1)) { +- *ppv = static_cast(this); +- } else { +- *ppv = NULL; +- return E_NOINTERFACE; +- } +- +- AddRef(); +- return S_OK; +- } ++ virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, LPVOID *ppv) { return E_NOINTERFACE; } + virtual ULONG STDMETHODCALLTYPE AddRef(void) { return 1; } + virtual ULONG STDMETHODCALLTYPE Release(void) { return 1; } + }; +@@ -769,7 +739,7 @@ static int decklink_write_video_packet(AVFormatContext *avctx, AVPacket *pkt) + ctx->first_pts = pkt->pts; + + /* Schedule frame for playback. */ +- hr = ctx->dlo->ScheduleVideoFrame(frame, ++ hr = ctx->dlo->ScheduleVideoFrame((class IDeckLinkVideoFrame *) frame, + pkt->pts * ctx->bmd_tb_num, + ctx->bmd_tb_num, ctx->bmd_tb_den); + /* Pass ownership to DeckLink, or release on failure */ +@@ -904,7 +874,7 @@ av_cold int ff_decklink_write_header(AVFormatContext *avctx) + return ret; + + /* Get output device. */ +- if (ctx->dl->QueryInterface(IID_IDeckLinkOutput_v14_2_1, (void **) &ctx->dlo) != S_OK) { ++ if (ctx->dl->QueryInterface(IID_IDeckLinkOutput, (void **) &ctx->dlo) != S_OK) { + av_log(avctx, AV_LOG_ERROR, "Could not open output device from '%s'\n", + avctx->url); + ret = AVERROR(EIO); diff --git a/services/capture/patch_decklink.py b/services/capture/patch_decklink.py index c032227..6ed9be1 100644 --- a/services/capture/patch_decklink.py +++ b/services/capture/patch_decklink.py @@ -1,51 +1,21 @@ -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 +#!/usr/bin/env python3 +# Apply the upstream FFmpeg master decklink SDK-16 compatibility patch on top +# of the release/7.1 source. The patch renames every IDeckLink* interface and +# helper to its _v14_2_1 versioned form so the call sites keep working against +# SDK 16's headers (which only retain the versioned aliases). Cherry-picking +# individual replacements like the previous regex patch produced inconsistent +# code that compiled but silently dropped every video frame. +import subprocess, sys, pathlib +patch = pathlib.Path('/decklink-sdk16.patch') +if not patch.exists(): + print('FATAL: /decklink-sdk16.patch not found in build context', file=sys.stderr) + sys.exit(1) +# Patch was produced as `git diff HEAD FETCH_HEAD` where HEAD=release/7.1 and +# FETCH_HEAD=master, so we apply it in REVERSE to move 7.1 → master. +result = subprocess.run( + ['git', 'apply', '-R', '--verbose', str(patch)], + cwd='/ffmpeg', capture_output=True, text=True, ) -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') +print(result.stdout) +print(result.stderr, file=sys.stderr) +sys.exit(result.returncode)