From 0ae9066737b4439e002d0fb18e276296b61b63b7 Mon Sep 17 00:00:00 2001 From: Julien Hery Date: Mon, 30 Mar 2026 18:02:13 -0400 Subject: [PATCH 1/2] Fix annotation export crash when viewed container is a Subset The annotations exporter accessed the playhead via session.viewed_container.playhead, which assumes the viewed container is always a Playlist. When media is loaded by a Python plugin (e.g. rdo_browser) that sets the viewer to a Subset, this crashes with "'Subset' object has no attribute 'playhead'" because the Subset class does not expose a playhead property. Replace all 5 call sites with self.current_playhead(), which is provided by PluginBase and returns the active playhead regardless of the viewed container type. --- .../annotations_exporter/annotations_exporter.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/plugin/python_plugins/annotations_exporter/annotations_exporter.py b/src/plugin/python_plugins/annotations_exporter/annotations_exporter.py index 2523e19a5..94b4fca43 100644 --- a/src/plugin/python_plugins/annotations_exporter/annotations_exporter.py +++ b/src/plugin/python_plugins/annotations_exporter/annotations_exporter.py @@ -159,7 +159,7 @@ def attribute_changed(self, attr, role): if attr == self.scope: if self.scope.value() in ["Current Media", "Current Frame"]: self.user_name.set_value( - self.connection.api.session.viewed_container.playhead.on_screen_media.name + self.current_playhead().on_screen_media.name ) elif self.scope.value() == "Current Playlist / Timeline": self.user_name.set_value( @@ -232,7 +232,7 @@ def do_export(self, scope, export_type, user_name, output_folder, file_type, res elif scope == "Current Media": self.export_media_annotations( - self.connection.api.session.viewed_container.playhead.on_screen_media + self.current_playhead().on_screen_media ) elif scope == "Current Playlist / Timeline": @@ -254,7 +254,7 @@ def do_export(self, scope, export_type, user_name, output_folder, file_type, res gp_file_path = self.__output_folder + "/greasePencil.xml" self.make_greaspencil_xml_file( gp_file_path, - self.connection.api.session.viewed_container.playhead.on_screen_media.media_source().rate.fps() + self.current_playhead().on_screen_media.media_source().rate.fps() ) # now we zip the folder final_name = shutil.make_archive(self.__output_folder + "/" + self.user_name.value(), 'zip', __tmp_folder) @@ -314,8 +314,8 @@ def export_frame(self, idx, frame, duration, bookmark, media): def export_bookmark_on_current_frame(self): - m = self.connection.api.session.viewed_container.playhead.on_screen_media - current_frame = self.connection.api.session.viewed_container.playhead.attributes['Media Logical Frame'].value() + m = self.current_playhead().on_screen_media + current_frame = self.current_playhead().attributes['Media Logical Frame'].value() bookmarks = m.ordered_bookmarks() bookmark = None for bm in bookmarks: From 5b4b05ceb2b77c1b96d4813019391853fa5149f0 Mon Sep 17 00:00:00 2001 From: Julien Hery Date: Wed, 1 Apr 2026 09:11:05 -0400 Subject: [PATCH 2/2] Fix Decklink Linux startup and add ABI fallback Port changes from xShirae/xstudio_bmd fix/decklink-linux-startup branch. Adds runtime detection info, v14.2.1 ABI fallback for Linux, improved error messages, proper resource cleanup, and null-safety checks. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../bmd_decklink/src/decklink_output.cpp | 180 +++++++++++++++--- .../bmd_decklink/src/decklink_output.hpp | 38 ++-- .../bmd_decklink/src/decklink_plugin.cpp | 19 +- .../src/extern/linux/DeckLinkAPIDispatch.cpp | 82 +++++++- 4 files changed, 270 insertions(+), 49 deletions(-) diff --git a/src/plugin/video_output/bmd_decklink/src/decklink_output.cpp b/src/plugin/video_output/bmd_decklink/src/decklink_output.cpp index e61005c18..683aae030 100644 --- a/src/plugin/video_output/bmd_decklink/src/decklink_output.cpp +++ b/src/plugin/video_output/bmd_decklink/src/decklink_output.cpp @@ -6,10 +6,15 @@ #include "xstudio/enums.hpp" #include #include +#include #ifdef __linux__ #include +#include "extern/linux/DeckLinkAPIVideoOutput_v14_2_1.h" #define kDeckLinkAPI_Name "libDeckLinkAPI.so" + +extern "C" const char *GetDeckLinkVideoConversionSymbolName(void); +extern "C" const char *GetDeckLinkAncillaryPacketsSymbolName(void); #endif using namespace xstudio::bm_decklink_plugin_1_0; @@ -48,8 +53,76 @@ class TimeLogger { std::string label_; std::chrono::high_resolution_clock::time_point start_time_; }; + +std::string with_runtime_details( + const std::string &message, const std::string &runtime_info) { + if (runtime_info.empty()) { + return message; + } + return fmt::format("{} ({})", message, runtime_info); +} } // namespace +void DecklinkOutput::detect_runtime_info() { + std::vector details; + +#ifdef __linux__ + Dl_info dl_info; + if (dladdr(reinterpret_cast(CreateDeckLinkIteratorInstance), &dl_info) && + dl_info.dli_fname) { + details.emplace_back(fmt::format("library={}", dl_info.dli_fname)); + } + + if (const auto *video_conversion_symbol = GetDeckLinkVideoConversionSymbolName()) { + details.emplace_back(fmt::format("video_conversion_symbol={}", video_conversion_symbol)); + } + + if (const auto *ancillary_packets_symbol = GetDeckLinkAncillaryPacketsSymbolName()) { + details.emplace_back( + fmt::format("ancillary_packets_symbol={}", ancillary_packets_symbol)); + } +#endif + + if (auto *api_info = CreateDeckLinkAPIInformationInstance()) { + const char *api_version = nullptr; + if (api_info->GetString(BMDDeckLinkAPIVersion, &api_version) == S_OK && api_version) { + details.emplace_back(fmt::format("api_version={}", api_version)); + } + api_info->Release(); + } + + if (details.empty()) { + runtime_info_.clear(); + return; + } + + std::ostringstream out; + for (size_t i = 0; i < details.size(); ++i) { + if (i) { + out << ", "; + } + out << details[i]; + } + runtime_info_ = out.str(); +} + +void DecklinkOutput::log_runtime_info() const { + if (runtime_info_.empty() && output_interface_info_.empty()) { + return; + } + + if (output_interface_info_.empty()) { + spdlog::info("DeckLink runtime detected: {}", runtime_info_); + } else if (runtime_info_.empty()) { + spdlog::info("DeckLink runtime detected: output_interface={}", output_interface_info_); + } else { + spdlog::info( + "DeckLink runtime detected: {}, output_interface={}", + runtime_info_, + output_interface_info_); + } +} + void DecklinkOutput::check_decklink_installation() { // here we try to open the decklink driver libs. If they are not installed @@ -117,6 +190,18 @@ DecklinkOutput::DecklinkOutput(BMDecklinkPlugin *decklink_xstudio_plugin) } DecklinkOutput::~DecklinkOutput() { + running_ = false; + { + std::lock_guard lk(audio_samples_cv_mutex_); + fetch_more_samples_from_xstudio_ = true; + } + audio_samples_cv_.notify_all(); + + release_resources(); + spdlog::info("Closing Decklink Output"); +} + +void DecklinkOutput::release_resources() { if (decklink_output_interface_ != NULL) { @@ -141,8 +226,13 @@ DecklinkOutput::~DecklinkOutput() { if (intermediate_frame_ != nullptr) { intermediate_frame_->Release(); + intermediate_frame_ = nullptr; } - spdlog::info("Closing Decklink Output"); + + output_callback_ = nullptr; + frame_converter_ = nullptr; + decklink_output_interface_ = nullptr; + decklink_interface_ = nullptr; } void DecklinkOutput::set_preroll() { @@ -245,8 +335,11 @@ bool DecklinkOutput::init_decklink() { bool bSuccess = false; IDeckLinkIterator *decklink_iterator = NULL; + last_error_.clear(); + output_interface_info_.clear(); try { + detect_runtime_info(); #ifdef _WIN32 HRESULT result; @@ -272,14 +365,34 @@ bool DecklinkOutput::init_decklink() { } if (decklink_iterator->Next(&decklink_interface_) != S_OK) { - throw std::runtime_error( - "This plugin requires a DeckLink device. You will not be able to use the " - "features of this plugin until a DeckLink device is installed."); + throw std::runtime_error(with_runtime_details( + "DeckLink drivers found but no device is installed", runtime_info_)); } if (decklink_interface_->QueryInterface( IID_IDeckLinkOutput, (void **)&decklink_output_interface_) != S_OK) { - throw std::runtime_error("QueryInterface failed."); +#ifdef __linux__ + IDeckLinkOutput_v14_2_1 *legacy_output_interface = nullptr; + if (decklink_interface_->QueryInterface( + IID_IDeckLinkOutput_v14_2_1, (void **)&legacy_output_interface) == S_OK && + legacy_output_interface != nullptr) { + decklink_output_interface_ = + reinterpret_cast(legacy_output_interface); + output_interface_info_ = "IID_IDeckLinkOutput_v14_2_1"; + spdlog::warn("DeckLink output is using the Linux v14.2.1 compatibility ABI."); + } else { + throw std::runtime_error(with_runtime_details( + "DeckLink runtime ABI mismatch: failed to query the video output " + "interface", + runtime_info_)); + } +#else + throw std::runtime_error(with_runtime_details( + "DeckLink runtime ABI mismatch: failed to query the video output interface", + runtime_info_)); +#endif + } else { + output_interface_info_ = "IID_IDeckLinkOutput"; } output_callback_ = new AVOutputCallback(this); @@ -310,38 +423,35 @@ bool DecklinkOutput::init_decklink() { #else frame_converter_ = CreateVideoConversionInstance(); + if (!frame_converter_) { + throw std::runtime_error(with_runtime_details( + "DeckLink runtime ABI mismatch: failed to create the video conversion " + "interface", + runtime_info_)); + } #endif - bSuccess = true; + bSuccess = true; + is_available_ = true; + log_runtime_info(); query_display_modes(); } catch (std::exception &e) { + is_available_ = false; + last_error_ = e.what(); std::cerr << "DecklinkOutput::init_decklink() failed: " << e.what() << "\n"; report_error(e.what()); - - if (decklink_output_interface_ != NULL) { - decklink_output_interface_->Release(); - decklink_output_interface_ = NULL; - } - if (decklink_interface_ != NULL) { - decklink_interface_->Release(); - decklink_interface_ = NULL; - } - if (output_callback_ != NULL) { - output_callback_->Release(); - output_callback_ = NULL; - } - - if (decklink_iterator != NULL) { - decklink_iterator->Release(); - decklink_iterator = NULL; - } + release_resources(); } + if (decklink_iterator != NULL) { + decklink_iterator->Release(); + decklink_iterator = NULL; + } return bSuccess; } @@ -351,6 +461,10 @@ void DecklinkOutput::query_display_modes() { IDeckLinkDisplayModeIterator *display_mode_iterator = NULL; IDeckLinkDisplayMode *display_mode = NULL; + if (!decklink_output_interface_) { + return; + } + try { // Get first avaliable video mode for Output @@ -437,7 +551,8 @@ bool DecklinkOutput::start_sdi_output() { try { if (!decklink_output_interface_) { - throw std::runtime_error("No DeckLink device is available."); + throw std::runtime_error( + last_error_.empty() ? "No DeckLink device is available." : last_error_); } bool mode_matched = false; @@ -542,6 +657,12 @@ bool DecklinkOutput::stop_sdi_output(const std::string &error_message) { decklink_output_interface_->DisableAudioOutput(); } + { + std::lock_guard lk(audio_samples_cv_mutex_); + fetch_more_samples_from_xstudio_ = true; + } + audio_samples_cv_.notify_all(); + mutex_.lock(); free(pFrameBuf); @@ -732,6 +853,10 @@ void DecklinkOutput::fill_decklink_video_frame(IDeckLinkVideoFrame *decklink_vid pFrame, src_buf, num_pix); } else { + if (!frame_converter_) { + throw std::runtime_error( + "DeckLink video conversion interface is unavailable."); + } // here we do our own conversion from 16 bit RGBA to 12 bit RGB // TimeLogger l("RGBA16_to_12bitRGBLE"); @@ -930,7 +1055,10 @@ long DecklinkOutput::num_samples_in_buffer() { void DecklinkOutput::copy_audio_samples_to_decklink_buffer(const bool /*preroll*/) { if (!decklink_output_interface_) { - fetch_more_samples_from_xstudio_ = true; + { + std::lock_guard m(audio_samples_cv_mutex_); + fetch_more_samples_from_xstudio_ = true; + } audio_samples_cv_.notify_one(); return; } diff --git a/src/plugin/video_output/bmd_decklink/src/decklink_output.hpp b/src/plugin/video_output/bmd_decklink/src/decklink_output.hpp index 4ce9a2715..aff2805ba 100644 --- a/src/plugin/video_output/bmd_decklink/src/decklink_output.hpp +++ b/src/plugin/video_output/bmd_decklink/src/decklink_output.hpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include "extern/decklink_compat.h" @@ -143,27 +144,33 @@ namespace bm_decklink_plugin_1_0 { } [[nodiscard]] bool is_available() const { return is_available_; } + [[nodiscard]] const std::string &last_error() const { return last_error_; } + [[nodiscard]] const std::string &runtime_info() const { return runtime_info_; } private: - AVOutputCallback *output_callback_; + void release_resources(); + void detect_runtime_info(); + void log_runtime_info() const; + + AVOutputCallback *output_callback_ = {nullptr}; std::mutex mutex_; - GLenum glStatus; - GLuint idFrameBuf, idColorBuf, idDepthBuf; - char *pFrameBuf; + GLenum glStatus = {0}; + GLuint idFrameBuf = {0}, idColorBuf = {0}, idDepthBuf = {0}; + char *pFrameBuf = {nullptr}; // DeckLink - uint32_t frame_width_; - uint32_t frame_height_; + uint32_t frame_width_ = {0}; + uint32_t frame_height_ = {0}; - IDeckLink *decklink_interface_; - IDeckLinkOutput *decklink_output_interface_; - IDeckLinkVideoConversion *frame_converter_; + IDeckLink *decklink_interface_ = {nullptr}; + IDeckLinkOutput *decklink_output_interface_ = {nullptr}; + IDeckLinkVideoConversion *frame_converter_ = {nullptr}; - BMDTimeValue frame_duration_; - BMDTimeScale frame_timescale_; - uint32_t uiFPS; - uint32_t uiTotalFrames; + BMDTimeValue frame_duration_ = {0}; + BMDTimeScale frame_timescale_ = {0}; + uint32_t uiFPS = {0}; + uint32_t uiTotalFrames = {0}; media_reader::ImageBufPtr current_frame_; std::mutex frames_mutex_; @@ -197,7 +204,10 @@ namespace bm_decklink_plugin_1_0 { HDRMetadata hdr_metadata_; std::mutex hdr_metadata_mutex_; - bool is_available_ = {false}; + bool is_available_ = {false}; + std::string last_error_ = {}; + std::string runtime_info_ = {}; + std::string output_interface_info_ = {}; }; class AVOutputCallback : public IDeckLinkVideoOutputCallback, diff --git a/src/plugin/video_output/bmd_decklink/src/decklink_plugin.cpp b/src/plugin/video_output/bmd_decklink/src/decklink_plugin.cpp index 7d147f4fc..cc65e4044 100644 --- a/src/plugin/video_output/bmd_decklink/src/decklink_plugin.cpp +++ b/src/plugin/video_output/bmd_decklink/src/decklink_plugin.cpp @@ -158,7 +158,9 @@ BMDecklinkPlugin::BMDecklinkPlugin( // This method is called when a new image buffer is ready to be displayed void BMDecklinkPlugin::incoming_video_frame_callback(media_reader::ImageBufPtr incoming) { - dcl_output_->incoming_frame(incoming); + if (dcl_output_ && dcl_output_->is_available()) { + dcl_output_->incoming_frame(incoming); + } } void BMDecklinkPlugin::exit_cleanup() { @@ -166,6 +168,7 @@ void BMDecklinkPlugin::exit_cleanup() { // instance. The BMDecklinkPlugin will therefore never get deleted due to // circular dependency so we use the on_exit delete dcl_output_; + dcl_output_ = nullptr; } void BMDecklinkPlugin::receive_status_callback(const utility::JsonStore &status_data) { @@ -296,6 +299,20 @@ void BMDecklinkPlugin::initialise() { try { dcl_output_ = new DecklinkOutput(this); + + if (!dcl_output_->is_available()) { + const auto decklink_error = + dcl_output_->last_error().empty() ? "No DeckLink device detected." + : dcl_output_->last_error(); + delete dcl_output_; + dcl_output_ = nullptr; + status_message_->set_value( + decklink_error); + is_in_error_->set_value(true); + spdlog::warn("Decklink output unavailable: {}", decklink_error); + return; + } + set_hdr_mode_and_metadata(); if (!dcl_output_->is_available()) { diff --git a/src/plugin/video_output/bmd_decklink/src/extern/linux/DeckLinkAPIDispatch.cpp b/src/plugin/video_output/bmd_decklink/src/extern/linux/DeckLinkAPIDispatch.cpp index 485117887..a7916a4a2 100644 --- a/src/plugin/video_output/bmd_decklink/src/extern/linux/DeckLinkAPIDispatch.cpp +++ b/src/plugin/video_output/bmd_decklink/src/extern/linux/DeckLinkAPIDispatch.cpp @@ -41,6 +41,7 @@ #include #include #include +#include #include "DeckLinkAPI.h" @@ -69,6 +70,58 @@ static CreateVideoConversionInstanceFunc gCreateVideoConversionFunc = NULL; static CreateDeckLinkDiscoveryInstanceFunc gCreateDeckLinkDiscoveryFunc = NULL; static CreateVideoFrameAncillaryPacketsInstanceFunc gCreateVideoFrameAncillaryPacketsFunc = NULL; +static const char *gVideoConversionSymbolName = NULL; +static const char *gAncillaryPacketsSymbolName = NULL; + +static void *GetSymbolAddress( + void *libraryHandle, + const char *primarySymbol, + const char *fallbackSymbol, + const char *symbolDescription) { + dlerror(); + void *symbol = dlsym(libraryHandle, primarySymbol); + if (!dlerror() && symbol) { + if (fallbackSymbol && strcmp(primarySymbol, "CreateVideoConversionInstance_0002") == 0) { + gVideoConversionSymbolName = primarySymbol; + } else if ( + fallbackSymbol && + strcmp(primarySymbol, "CreateVideoFrameAncillaryPacketsInstance_0002") == 0) { + gAncillaryPacketsSymbolName = primarySymbol; + } + return symbol; + } + + if (!fallbackSymbol) { + fprintf(stderr, "DeckLink API missing symbol %s\n", primarySymbol); + return NULL; + } + + dlerror(); + symbol = dlsym(libraryHandle, fallbackSymbol); + if (!dlerror() && symbol) { + if (strcmp(primarySymbol, "CreateVideoConversionInstance_0002") == 0) { + gVideoConversionSymbolName = fallbackSymbol; + } else if ( + strcmp(primarySymbol, "CreateVideoFrameAncillaryPacketsInstance_0002") == 0) { + gAncillaryPacketsSymbolName = fallbackSymbol; + } + fprintf( + stderr, + "DeckLink API using compatibility %s symbol %s (preferred %s unavailable)\n", + symbolDescription, + fallbackSymbol, + primarySymbol); + return symbol; + } + + fprintf( + stderr, + "DeckLink API missing %s symbols %s and %s\n", + symbolDescription, + primarySymbol, + fallbackSymbol); + return NULL; +} static void InitDeckLinkAPI(void) { void *libraryHandle; @@ -89,18 +142,21 @@ static void InitDeckLinkAPI(void) { libraryHandle, "CreateDeckLinkAPIInformationInstance_0001"); if (!gCreateAPIInformationFunc) fprintf(stderr, "%s\n", dlerror()); - gCreateVideoConversionFunc = (CreateVideoConversionInstanceFunc)dlsym( - libraryHandle, "CreateVideoConversionInstance_0002"); - if (!gCreateVideoConversionFunc) - fprintf(stderr, "%s\n", dlerror()); + gCreateVideoConversionFunc = (CreateVideoConversionInstanceFunc)GetSymbolAddress( + libraryHandle, + "CreateVideoConversionInstance_0002", + "CreateVideoConversionInstance_0001", + "video conversion"); gCreateDeckLinkDiscoveryFunc = (CreateDeckLinkDiscoveryInstanceFunc)dlsym( libraryHandle, "CreateDeckLinkDiscoveryInstance_0003"); if (!gCreateDeckLinkDiscoveryFunc) fprintf(stderr, "%s\n", dlerror()); - gCreateVideoFrameAncillaryPacketsFunc = (CreateVideoFrameAncillaryPacketsInstanceFunc)dlsym( - libraryHandle, "CreateVideoFrameAncillaryPacketsInstance_0002"); - if (!gCreateVideoFrameAncillaryPacketsFunc) - fprintf(stderr, "%s\n", dlerror()); + gCreateVideoFrameAncillaryPacketsFunc = + (CreateVideoFrameAncillaryPacketsInstanceFunc)GetSymbolAddress( + libraryHandle, + "CreateVideoFrameAncillaryPacketsInstance_0002", + "CreateVideoFrameAncillaryPacketsInstance_0001", + "ancillary packets"); } static void InitDeckLinkPreviewAPI(void) { @@ -121,6 +177,16 @@ static void InitDeckLinkPreviewAPI(void) { fprintf(stderr, "%s\n", dlerror()); } +extern "C" const char *GetDeckLinkVideoConversionSymbolName(void) { + pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI); + return gVideoConversionSymbolName; +} + +extern "C" const char *GetDeckLinkAncillaryPacketsSymbolName(void) { + pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI); + return gAncillaryPacketsSymbolName; +} + bool IsDeckLinkAPIPresent(void) { // If the DeckLink API dynamic library was successfully loaded, return this knowledge to the // caller