From 312d71f7e8081e189e569369ebc86d6cd76cd987 Mon Sep 17 00:00:00 2001 From: xShirae Date: Wed, 1 Apr 2026 12:07:17 +0200 Subject: [PATCH 01/12] Fix Decklink Linux startup and add ABI fallback --- .../bmd_decklink/src/decklink_output.cpp | 187 +++++++++++++++--- .../bmd_decklink/src/decklink_output.hpp | 43 ++-- .../bmd_decklink/src/decklink_plugin.cpp | 19 +- .../src/extern/linux/DeckLinkAPIDispatch.cpp | 82 +++++++- 4 files changed, 285 insertions(+), 46 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..0461ec867 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,12 @@ bool DecklinkOutput::start_sdi_output() { try { if (!decklink_output_interface_) { +<<<<<<< HEAD throw std::runtime_error("No DeckLink device is available."); +======= + throw std::runtime_error( + last_error_.empty() ? "No DeckLink device is available." : last_error_); +>>>>>>> 6d529ba (Fix Decklink Linux startup and add ABI fallback) } bool mode_matched = false; @@ -541,6 +660,15 @@ bool DecklinkOutput::stop_sdi_output(const std::string &error_message) { decklink_output_interface_->DisableVideoOutput(); decklink_output_interface_->DisableAudioOutput(); } +<<<<<<< HEAD +======= + + { + std::lock_guard lk(audio_samples_cv_mutex_); + fetch_more_samples_from_xstudio_ = true; + } + audio_samples_cv_.notify_all(); +>>>>>>> 6d529ba (Fix Decklink Linux startup and add ABI fallback) mutex_.lock(); @@ -732,6 +860,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 +1062,14 @@ long DecklinkOutput::num_samples_in_buffer() { void DecklinkOutput::copy_audio_samples_to_decklink_buffer(const bool /*preroll*/) { if (!decklink_output_interface_) { +<<<<<<< HEAD fetch_more_samples_from_xstudio_ = true; +======= + { + std::lock_guard m(audio_samples_cv_mutex_); + fetch_more_samples_from_xstudio_ = true; + } +>>>>>>> 6d529ba (Fix Decklink Linux startup and add ABI fallback) 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..502203a8b 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,36 @@ namespace bm_decklink_plugin_1_0 { } [[nodiscard]] bool is_available() const { return is_available_; } +<<<<<<< HEAD +======= + [[nodiscard]] const std::string &last_error() const { return last_error_; } + [[nodiscard]] const std::string &runtime_info() const { return runtime_info_; } +>>>>>>> 6d529ba (Fix Decklink Linux startup and add ABI fallback) 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 +207,14 @@ namespace bm_decklink_plugin_1_0 { HDRMetadata hdr_metadata_; std::mutex hdr_metadata_mutex_; +<<<<<<< HEAD bool is_available_ = {false}; +======= + bool is_available_ = {false}; + std::string last_error_ = {}; + std::string runtime_info_ = {}; + std::string output_interface_info_ = {}; +>>>>>>> 6d529ba (Fix Decklink Linux startup and add ABI fallback) }; 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..5f8cc8b6a 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) { @@ -127,6 +183,16 @@ bool IsDeckLinkAPIPresent(void) { return gLoadedDeckLinkAPI; } +extern "C" const char *GetDeckLinkVideoConversionSymbolName(void) { + pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI); + return gVideoConversionSymbolName; +} + +extern "C" const char *GetDeckLinkAncillaryPacketsSymbolName(void) { + pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI); + return gAncillaryPacketsSymbolName; +} + IDeckLinkIterator *CreateDeckLinkIteratorInstance(void) { pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI); From b5bc09f3659d9be7adf305ee0d9793a9a304767b Mon Sep 17 00:00:00 2001 From: xShirae Date: Wed, 1 Apr 2026 16:17:12 +0200 Subject: [PATCH 02/12] Fix merge conflicts --- .../bmd_decklink/src/decklink_output.cpp | 11 ----------- .../bmd_decklink/src/decklink_output.hpp | 13 +++---------- 2 files changed, 3 insertions(+), 21 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 0461ec867..683aae030 100644 --- a/src/plugin/video_output/bmd_decklink/src/decklink_output.cpp +++ b/src/plugin/video_output/bmd_decklink/src/decklink_output.cpp @@ -551,12 +551,8 @@ bool DecklinkOutput::start_sdi_output() { try { if (!decklink_output_interface_) { -<<<<<<< HEAD - throw std::runtime_error("No DeckLink device is available."); -======= throw std::runtime_error( last_error_.empty() ? "No DeckLink device is available." : last_error_); ->>>>>>> 6d529ba (Fix Decklink Linux startup and add ABI fallback) } bool mode_matched = false; @@ -660,15 +656,12 @@ bool DecklinkOutput::stop_sdi_output(const std::string &error_message) { decklink_output_interface_->DisableVideoOutput(); decklink_output_interface_->DisableAudioOutput(); } -<<<<<<< HEAD -======= { std::lock_guard lk(audio_samples_cv_mutex_); fetch_more_samples_from_xstudio_ = true; } audio_samples_cv_.notify_all(); ->>>>>>> 6d529ba (Fix Decklink Linux startup and add ABI fallback) mutex_.lock(); @@ -1062,14 +1055,10 @@ long DecklinkOutput::num_samples_in_buffer() { void DecklinkOutput::copy_audio_samples_to_decklink_buffer(const bool /*preroll*/) { if (!decklink_output_interface_) { -<<<<<<< HEAD - fetch_more_samples_from_xstudio_ = true; -======= { std::lock_guard m(audio_samples_cv_mutex_); fetch_more_samples_from_xstudio_ = true; } ->>>>>>> 6d529ba (Fix Decklink Linux startup and add ABI fallback) 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 502203a8b..9952576e3 100644 --- a/src/plugin/video_output/bmd_decklink/src/decklink_output.hpp +++ b/src/plugin/video_output/bmd_decklink/src/decklink_output.hpp @@ -144,11 +144,8 @@ namespace bm_decklink_plugin_1_0 { } [[nodiscard]] bool is_available() const { return is_available_; } -<<<<<<< HEAD -======= [[nodiscard]] const std::string &last_error() const { return last_error_; } [[nodiscard]] const std::string &runtime_info() const { return runtime_info_; } ->>>>>>> 6d529ba (Fix Decklink Linux startup and add ABI fallback) private: void release_resources(); @@ -207,14 +204,10 @@ namespace bm_decklink_plugin_1_0 { HDRMetadata hdr_metadata_; std::mutex hdr_metadata_mutex_; -<<<<<<< HEAD - bool is_available_ = {false}; -======= - bool is_available_ = {false}; - std::string last_error_ = {}; - std::string runtime_info_ = {}; + bool is_available_ = {false}; + std::string last_error_ = {}; + std::string runtime_info_ = {}; std::string output_interface_info_ = {}; ->>>>>>> 6d529ba (Fix Decklink Linux startup and add ABI fallback) }; class AVOutputCallback : public IDeckLinkVideoOutputCallback, From 9077af4d159dfe45115da9017850eac6c850cd8b Mon Sep 17 00:00:00 2001 From: "florence.beliveau" Date: Wed, 1 Apr 2026 18:04:33 +0200 Subject: [PATCH 03/12] Fix DeckLink callback registration and partial init cleanup --- .../bmd_decklink/src/decklink_output.cpp | 2212 +++++++++-------- .../bmd_decklink/src/decklink_output.hpp | 473 ++-- 2 files changed, 1370 insertions(+), 1315 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 683aae030..a1a8b866d 100644 --- a/src/plugin/video_output/bmd_decklink/src/decklink_output.cpp +++ b/src/plugin/video_output/bmd_decklink/src/decklink_output.cpp @@ -1,603 +1,617 @@ -// SPDX-License-Identifier: Apache-2.0 -#include "decklink_output.hpp" -#include "decklink_plugin.hpp" +// SPDX-License-Identifier: Apache-2.0 +#include "decklink_output.hpp" +#include "decklink_plugin.hpp" #include "xstudio/utility/logging.hpp" #include "xstudio/utility/chrono.hpp" #include "xstudio/enums.hpp" +#include #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; - -// Uncomment to use this debug timer to see how the frame conversion is performing -namespace { - -class LogBot { - public: - std::map> frame_times_; - void log(const std::string l, int64_t t) { - frame_times_[l].push_back(t); - if ((frame_times_[l].size() % 24) == 0) { - int64_t total = 0; - for (auto v : frame_times_[l]) - total += v; - std::cerr << "Average time for " << l << ": " - << double(total / frame_times_[l].size()) / 1000.0 << "ms\n"; - frame_times_[l].clear(); - } - } -}; -static LogBot s_logBot; - -class TimeLogger { - public: - TimeLogger(const std::string &label) - : label_(label), start_time_(std::chrono::high_resolution_clock::now()) {} - ~TimeLogger() { - s_logBot.log( - label_, - std::chrono::duration_cast( - std::chrono::high_resolution_clock::now() - start_time_) - .count()); - } - 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 - // on the system abort construction of the plugin (caught by plugin manager) - // The reason we do this is that we don't want the plugin to be available at - // all in the UI if the drivers aren't present, as it would just lead to - // user confusion and support requests about why the plugin doesn't work. -#ifdef __APPLE__ - CFURLRef bundleURL = CFURLCreateWithFileSystemPath( - kCFAllocatorDefault, - CFSTR("/Library/Frameworks/DeckLinkAPI.framework"), - kCFURLPOSIXPathStyle, - true); - bool drivers_found = false; - if (bundleURL) { - CFBundleRef bundle = CFBundleCreate(kCFAllocatorDefault, bundleURL); - drivers_found = (bundle != NULL); - if (bundle) - CFRelease(bundle); - CFRelease(bundleURL); - } - if (!drivers_found) { - throw std::runtime_error("drivers not found."); - return; - } -#elif defined(__linux__) - if (!dlopen(kDeckLinkAPI_Name, RTLD_NOW | RTLD_GLOBAL)) { - throw std::runtime_error("drivers not found."); - return; - } -#else // Windows - IDeckLinkIterator *pDLIterator = NULL; - HRESULT result; - result = CoCreateInstance( - CLSID_CDeckLinkIterator, - NULL, - CLSCTX_ALL, - IID_IDeckLinkIterator, - (void **)&pDLIterator); - if (FAILED(result)) { - throw std::runtime_error("drivers not found."); - } - - // If no device found, that will be caught later. - /*IDeckLink* deckLink = nullptr; - if (pDLIterator->Next(&deckLink) != S_OK) - { - if (deckLink != NULL) - { - deckLink->Release(); - } else { - throw std::runtime_error("no DeckLink devices found."); - } - }*/ -#endif -} - -DecklinkOutput::DecklinkOutput(BMDecklinkPlugin *decklink_xstudio_plugin) - : pFrameBuf(NULL), - decklink_interface_(NULL), - decklink_output_interface_(NULL), - decklink_xstudio_plugin_(decklink_xstudio_plugin) { - - is_available_ = init_decklink(); -} - -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"); -} - +#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; + +// Uncomment to use this debug timer to see how the frame conversion is performing +namespace { + +class LogBot { + public: + std::map> frame_times_; + void log(const std::string l, int64_t t) { + frame_times_[l].push_back(t); + if ((frame_times_[l].size() % 24) == 0) { + int64_t total = 0; + for (auto v : frame_times_[l]) + total += v; + std::cerr << "Average time for " << l << ": " + << double(total / frame_times_[l].size()) / 1000.0 << "ms\n"; + frame_times_[l].clear(); + } + } +}; +static LogBot s_logBot; + +class TimeLogger { + public: + TimeLogger(const std::string &label) + : label_(label), start_time_(std::chrono::high_resolution_clock::now()) {} + ~TimeLogger() { + s_logBot.log( + label_, + std::chrono::duration_cast( + std::chrono::high_resolution_clock::now() - start_time_) + .count()); + } + 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 + // on the system abort construction of the plugin (caught by plugin manager) + // The reason we do this is that we don't want the plugin to be available at + // all in the UI if the drivers aren't present, as it would just lead to + // user confusion and support requests about why the plugin doesn't work. +#ifdef __APPLE__ + CFURLRef bundleURL = CFURLCreateWithFileSystemPath( + kCFAllocatorDefault, + CFSTR("/Library/Frameworks/DeckLinkAPI.framework"), + kCFURLPOSIXPathStyle, + true); + bool drivers_found = false; + if (bundleURL) { + CFBundleRef bundle = CFBundleCreate(kCFAllocatorDefault, bundleURL); + drivers_found = (bundle != NULL); + if (bundle) + CFRelease(bundle); + CFRelease(bundleURL); + } + if (!drivers_found) { + throw std::runtime_error("drivers not found."); + return; + } +#elif defined(__linux__) + if (!dlopen(kDeckLinkAPI_Name, RTLD_NOW | RTLD_GLOBAL)) { + throw std::runtime_error("drivers not found."); + return; + } +#else // Windows + IDeckLinkIterator *pDLIterator = NULL; + HRESULT result; + result = CoCreateInstance( + CLSID_CDeckLinkIterator, + NULL, + CLSCTX_ALL, + IID_IDeckLinkIterator, + (void **)&pDLIterator); + if (FAILED(result)) { + throw std::runtime_error("drivers not found."); + } + + // If no device found, that will be caught later. + /*IDeckLink* deckLink = nullptr; + if (pDLIterator->Next(&deckLink) != S_OK) + { + if (deckLink != NULL) + { + deckLink->Release(); + } else { + throw std::runtime_error("no DeckLink devices found."); + } + }*/ +#endif +} + +DecklinkOutput::DecklinkOutput(BMDecklinkPlugin *decklink_xstudio_plugin) + : pFrameBuf(NULL), + decklink_interface_(NULL), + decklink_output_interface_(NULL), + decklink_xstudio_plugin_(decklink_xstudio_plugin) { + + is_available_ = init_decklink(); +} + +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) { spdlog::info("Stopping Decklink output loop."); - decklink_output_interface_->StopScheduledPlayback(0, NULL, 0); - decklink_output_interface_->DisableVideoOutput(); - decklink_output_interface_->DisableAudioOutput(); + if (scheduled_playback_started_) { + decklink_output_interface_->StopScheduledPlayback(0, NULL, 0); + scheduled_playback_started_ = false; + } + if (video_output_enabled_) { + decklink_output_interface_->DisableVideoOutput(); + video_output_enabled_ = false; + } + if (audio_output_enabled_) { + decklink_output_interface_->DisableAudioOutput(); + audio_output_enabled_ = false; + } decklink_output_interface_->Release(); } - if (decklink_interface_ != NULL) { - decklink_interface_->Release(); - } - if (output_callback_ != NULL) { - output_callback_->Release(); - } - - if (frame_converter_ != NULL) { - frame_converter_->Release(); - } - - if (intermediate_frame_ != nullptr) { - intermediate_frame_->Release(); - intermediate_frame_ = nullptr; - } - + if (decklink_interface_ != NULL) { + decklink_interface_->Release(); + } + if (output_callback_ != NULL) { + output_callback_->Release(); + } + + if (frame_converter_ != NULL) { + frame_converter_->Release(); + } + + if (intermediate_frame_ != nullptr) { + intermediate_frame_->Release(); + intermediate_frame_ = nullptr; + } + output_callback_ = nullptr; frame_converter_ = nullptr; decklink_output_interface_ = nullptr; decklink_interface_ = nullptr; + scheduled_playback_started_ = false; + video_output_enabled_ = false; + audio_output_enabled_ = false; } - -void DecklinkOutput::set_preroll() { - IDeckLinkMutableVideoFrame *decklink_video_frame = NULL; - - // Set 3 frame preroll - try { - - for (uint32_t i = 0; i < 3; i++) { - - int32_t rowBytes; - if (decklink_output_interface_->RowBytesForPixelFormat( - current_pix_format_, frame_width_, &rowBytes) != S_OK) { - throw std::runtime_error("Failed on call to RowBytesForPixelFormat."); - } - - // Flip frame vertical, because OpenGL rendering starts from left bottom corner - if (decklink_output_interface_->CreateVideoFrame( - frame_width_, - frame_height_, - rowBytes, - current_pix_format_, - bmdFrameFlagFlipVertical, - &decklink_video_frame) != S_OK) - throw std::runtime_error("Failed on CreateVideoFrame"); - - update_frame_metadata(decklink_video_frame); - - if (decklink_output_interface_->ScheduleVideoFrame( - decklink_video_frame, - (uiTotalFrames * frame_duration_), - frame_duration_, - frame_timescale_) != S_OK) - throw std::runtime_error("Failed on ScheduleVideoFrame"); - - /* The local reference to the IDeckLinkVideoFrame is released here, as the ownership - * has now been passed to the DeckLinkAPI via ScheduleVideoFrame. - * - * After the API has finished with the frame, it is returned to the application via - * ScheduledFrameCompleted. In ScheduledFrameCompleted, this application updates the - * video frame and passes it to ScheduleVideoFrame, returning ownership to the - * DeckLink API. - */ - decklink_video_frame->Release(); - decklink_video_frame = NULL; - - uiTotalFrames++; - } - } catch (std::exception &e) { - - if (decklink_video_frame) { - decklink_video_frame->Release(); - decklink_video_frame = NULL; - } - report_error(e.what()); - } -} - -void DecklinkOutput::make_intermediate_frame() { - - // We need to support some othe pixel formats less likely to be needed but - // nevertheless possibly required. For this we convert to BMD 12 bit RGB - // (which seems to be the most efficient conversion on our side) and then - // use Decklink API to convert from that to the desired output format. - int32_t referenceBytesPerRow; - - HRESULT result = decklink_output_interface_->RowBytesForPixelFormat( - bmdFormat12BitRGBLE, frame_width_, &referenceBytesPerRow); - - if (result != S_OK) { - throw std::runtime_error("Failed to get row bytes for reference video frame"); - } - - if (intermediate_frame_) { - if (intermediate_frame_->GetWidth() != frame_width_ || - intermediate_frame_->GetHeight() != frame_height_) { - intermediate_frame_->Release(); - intermediate_frame_ = nullptr; - } - } - - if (!intermediate_frame_) { - // Create a black frame in 8 bit YUV and convert to desired format - result = decklink_output_interface_->CreateVideoFrame( - frame_width_, - frame_height_, - referenceBytesPerRow, - bmdFormat12BitRGBLE, - bmdFrameFlagDefault, - &intermediate_frame_); - - if (result != S_OK) { - throw std::runtime_error("Failed to create reference video frame"); - } - } -} - - -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; - result = CoCreateInstance( - CLSID_CDeckLinkIterator, - NULL, - CLSCTX_ALL, - IID_IDeckLinkIterator, - (void **)&decklink_iterator); - if (FAILED(result)) { - throw std::runtime_error( - "Please install the Blackmagic DeckLink drivers to use the features of this " - "application.This application requires the DeckLink drivers installed."); - return false; - } -#else - decklink_iterator = CreateDeckLinkIteratorInstance(); -#endif - if (decklink_iterator == NULL) { - throw std::runtime_error( - "This plugin requires the DeckLink drivers installed. Please install the " - "Blackmagic DeckLink drivers to use the features of this plugin."); - } - - if (decklink_iterator->Next(&decklink_interface_) != S_OK) { - 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) { -#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); - if (output_callback_ == NULL) - throw std::runtime_error("Failed to create Video Output Callback."); - - if (decklink_output_interface_->SetScheduledFrameCompletionCallback(output_callback_) != - S_OK) - throw std::runtime_error("SetScheduledFrameCompletionCallback failed."); - - if (decklink_output_interface_->SetAudioCallback(output_callback_) != S_OK) - throw std::runtime_error("SetAudioCallback failed."); - -#ifdef _WIN32 - // Create an IDeckLinkVideoConversion interface object to provide pixel format - // conversion of video frame. - result = CoCreateInstance( - CLSID_CDeckLinkVideoConversion, - NULL, - CLSCTX_ALL, - IID_IDeckLinkVideoConversion, - (void **)&frame_converter_); - if (FAILED(result)) { - throw std::runtime_error( - "A DeckLink Video Conversion interface could not be created."); - } - -#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; - 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()); - release_resources(); - } - - if (decklink_iterator != NULL) { - decklink_iterator->Release(); - decklink_iterator = NULL; - } - - return bSuccess; -} - -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 - if (decklink_output_interface_->GetDisplayModeIterator(&display_mode_iterator) == - S_OK) { - - while (display_mode_iterator->Next(&display_mode) == S_OK) { - - DECKLINK_STR modeName = nullptr; - display_mode->GetName(&modeName); - std::string buf = decklink_string_to_std(modeName); - decklink_free_string(modeName); - display_mode->GetFrameRate(&frame_duration_, &frame_timescale_); - - // only names with 'i' in are interalaced as far as I can tell - const bool interlaced = buf.find("i") != std::string::npos; - - // I've decided that support for interlaced modes is not useful! - if (interlaced) - continue; - - const std::string resolution_string = - fmt::format("{} x {}", display_mode->GetWidth(), display_mode->GetHeight()); - std::string refresh_rate = - fmt::format("{:.3f}", double(frame_timescale_) / double(frame_duration_)); - // erase all but the last trailing zero - while (refresh_rate.back() == '0' && - refresh_rate.rfind(".0") != (refresh_rate.size() - 2)) { - refresh_rate.pop_back(); - } - - refresh_rate_per_output_resolution_[resolution_string].push_back(refresh_rate); - display_modes_[std::make_pair(resolution_string, refresh_rate)] = - display_mode->GetDisplayMode(); - } - } - } catch (std::exception &e) { - - report_error(e.what()); - } - - if (display_mode) { - display_mode->Release(); - display_mode = NULL; - } - - if (display_mode_iterator) { - display_mode_iterator->Release(); - display_mode_iterator = NULL; - } -} - -std::vector -DecklinkOutput::get_available_refresh_rates(const std::string &output_resolution) const { - auto p = refresh_rate_per_output_resolution_.find(output_resolution); - if (p != refresh_rate_per_output_resolution_.end()) { - return p->second; - } - return std::vector({"Bad Resolution"}); -} - -void DecklinkOutput::set_display_mode( - const std::string &resolution, - const std::string &refresh_rate, - const BMDPixelFormat pix_format) { - - auto p = display_modes_.find(std::make_pair(resolution, refresh_rate)); - if (p == display_modes_.end()) { - throw std::runtime_error( - fmt::format("Failed to find a display mode for {} @ {}", resolution, refresh_rate)); - } - current_pix_format_ = pix_format; - current_display_mode_ = p->second; -} - - -bool DecklinkOutput::start_sdi_output() { - - bool bSuccess = false; - - IDeckLinkDisplayModeIterator *display_mode_iterator = NULL; - IDeckLinkDisplayMode *display_mode = NULL; - - try { - + +void DecklinkOutput::set_preroll() { + IDeckLinkMutableVideoFrame *decklink_video_frame = NULL; + + // Set 3 frame preroll + try { + + for (uint32_t i = 0; i < 3; i++) { + + int32_t rowBytes; + if (decklink_output_interface_->RowBytesForPixelFormat( + current_pix_format_, frame_width_, &rowBytes) != S_OK) { + throw std::runtime_error("Failed on call to RowBytesForPixelFormat."); + } + + // Flip frame vertical, because OpenGL rendering starts from left bottom corner + if (decklink_output_interface_->CreateVideoFrame( + frame_width_, + frame_height_, + rowBytes, + current_pix_format_, + bmdFrameFlagFlipVertical, + &decklink_video_frame) != S_OK) + throw std::runtime_error("Failed on CreateVideoFrame"); + + update_frame_metadata(decklink_video_frame); + + if (decklink_output_interface_->ScheduleVideoFrame( + decklink_video_frame, + (uiTotalFrames * frame_duration_), + frame_duration_, + frame_timescale_) != S_OK) + throw std::runtime_error("Failed on ScheduleVideoFrame"); + + /* The local reference to the IDeckLinkVideoFrame is released here, as the ownership + * has now been passed to the DeckLinkAPI via ScheduleVideoFrame. + * + * After the API has finished with the frame, it is returned to the application via + * ScheduledFrameCompleted. In ScheduledFrameCompleted, this application updates the + * video frame and passes it to ScheduleVideoFrame, returning ownership to the + * DeckLink API. + */ + decklink_video_frame->Release(); + decklink_video_frame = NULL; + + uiTotalFrames++; + } + } catch (std::exception &e) { + + if (decklink_video_frame) { + decklink_video_frame->Release(); + decklink_video_frame = NULL; + } + report_error(e.what()); + } +} + +void DecklinkOutput::make_intermediate_frame() { + + // We need to support some othe pixel formats less likely to be needed but + // nevertheless possibly required. For this we convert to BMD 12 bit RGB + // (which seems to be the most efficient conversion on our side) and then + // use Decklink API to convert from that to the desired output format. + int32_t referenceBytesPerRow; + + HRESULT result = decklink_output_interface_->RowBytesForPixelFormat( + bmdFormat12BitRGBLE, frame_width_, &referenceBytesPerRow); + + if (result != S_OK) { + throw std::runtime_error("Failed to get row bytes for reference video frame"); + } + + if (intermediate_frame_) { + if (intermediate_frame_->GetWidth() != frame_width_ || + intermediate_frame_->GetHeight() != frame_height_) { + intermediate_frame_->Release(); + intermediate_frame_ = nullptr; + } + } + + if (!intermediate_frame_) { + // Create a black frame in 8 bit YUV and convert to desired format + result = decklink_output_interface_->CreateVideoFrame( + frame_width_, + frame_height_, + referenceBytesPerRow, + bmdFormat12BitRGBLE, + bmdFrameFlagDefault, + &intermediate_frame_); + + if (result != S_OK) { + throw std::runtime_error("Failed to create reference video frame"); + } + } +} + + +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; + result = CoCreateInstance( + CLSID_CDeckLinkIterator, + NULL, + CLSCTX_ALL, + IID_IDeckLinkIterator, + (void **)&decklink_iterator); + if (FAILED(result)) { + throw std::runtime_error( + "Please install the Blackmagic DeckLink drivers to use the features of this " + "application.This application requires the DeckLink drivers installed."); + return false; + } +#else + decklink_iterator = CreateDeckLinkIteratorInstance(); +#endif + if (decklink_iterator == NULL) { + throw std::runtime_error( + "This plugin requires the DeckLink drivers installed. Please install the " + "Blackmagic DeckLink drivers to use the features of this plugin."); + } + + if (decklink_iterator->Next(&decklink_interface_) != S_OK) { + 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) { +#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); + if (output_callback_ == NULL) + throw std::runtime_error("Failed to create Video Output Callback."); + + if (decklink_output_interface_->SetScheduledFrameCompletionCallback(output_callback_) != + S_OK) + throw std::runtime_error("SetScheduledFrameCompletionCallback failed."); + + if (decklink_output_interface_->SetAudioCallback(output_callback_) != S_OK) + throw std::runtime_error("SetAudioCallback failed."); + +#ifdef _WIN32 + // Create an IDeckLinkVideoConversion interface object to provide pixel format + // conversion of video frame. + result = CoCreateInstance( + CLSID_CDeckLinkVideoConversion, + NULL, + CLSCTX_ALL, + IID_IDeckLinkVideoConversion, + (void **)&frame_converter_); + if (FAILED(result)) { + throw std::runtime_error( + "A DeckLink Video Conversion interface could not be created."); + } + +#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; + 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()); + release_resources(); + } + + if (decklink_iterator != NULL) { + decklink_iterator->Release(); + decklink_iterator = NULL; + } + + return bSuccess; +} + +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 + if (decklink_output_interface_->GetDisplayModeIterator(&display_mode_iterator) == + S_OK) { + + while (display_mode_iterator->Next(&display_mode) == S_OK) { + + DECKLINK_STR modeName = nullptr; + display_mode->GetName(&modeName); + std::string buf = decklink_string_to_std(modeName); + decklink_free_string(modeName); + display_mode->GetFrameRate(&frame_duration_, &frame_timescale_); + + // only names with 'i' in are interalaced as far as I can tell + const bool interlaced = buf.find("i") != std::string::npos; + + // I've decided that support for interlaced modes is not useful! + if (interlaced) + continue; + + const std::string resolution_string = + fmt::format("{} x {}", display_mode->GetWidth(), display_mode->GetHeight()); + std::string refresh_rate = + fmt::format("{:.3f}", double(frame_timescale_) / double(frame_duration_)); + // erase all but the last trailing zero + while (refresh_rate.back() == '0' && + refresh_rate.rfind(".0") != (refresh_rate.size() - 2)) { + refresh_rate.pop_back(); + } + + refresh_rate_per_output_resolution_[resolution_string].push_back(refresh_rate); + display_modes_[std::make_pair(resolution_string, refresh_rate)] = + display_mode->GetDisplayMode(); + } + } + } catch (std::exception &e) { + + report_error(e.what()); + } + + if (display_mode) { + display_mode->Release(); + display_mode = NULL; + } + + if (display_mode_iterator) { + display_mode_iterator->Release(); + display_mode_iterator = NULL; + } +} + +std::vector +DecklinkOutput::get_available_refresh_rates(const std::string &output_resolution) const { + auto p = refresh_rate_per_output_resolution_.find(output_resolution); + if (p != refresh_rate_per_output_resolution_.end()) { + return p->second; + } + return std::vector({"Bad Resolution"}); +} + +void DecklinkOutput::set_display_mode( + const std::string &resolution, + const std::string &refresh_rate, + const BMDPixelFormat pix_format) { + + auto p = display_modes_.find(std::make_pair(resolution, refresh_rate)); + if (p == display_modes_.end()) { + throw std::runtime_error( + fmt::format("Failed to find a display mode for {} @ {}", resolution, refresh_rate)); + } + current_pix_format_ = pix_format; + current_display_mode_ = p->second; +} + + +bool DecklinkOutput::start_sdi_output() { + + bool bSuccess = false; + + IDeckLinkDisplayModeIterator *display_mode_iterator = NULL; + IDeckLinkDisplayMode *display_mode = NULL; + + try { + if (!decklink_output_interface_) { throw std::runtime_error( last_error_.empty() ? "No DeckLink device is available." : last_error_); } - - bool mode_matched = false; - // Get first avaliable video mode for Output - if (decklink_output_interface_->GetDisplayModeIterator(&display_mode_iterator) == - S_OK) { - while (display_mode_iterator->Next(&display_mode) == S_OK) { - if (display_mode->GetDisplayMode() == current_display_mode_) { - - mode_matched = true; - { - // get the name of the display mode, for display only - DECKLINK_STR modeName = nullptr; - display_mode->GetName(&modeName); - display_mode_name_ = decklink_string_to_std(modeName); - decklink_free_string(modeName); - } - - report_status( - fmt::format( - "Starting Decklink output loop in mode {}.", display_mode_name_), - false); - - frame_width_ = display_mode->GetWidth(); - frame_height_ = display_mode->GetHeight(); - display_mode->GetFrameRate(&frame_duration_, &frame_timescale_); - - uiFPS = ((frame_timescale_ + (frame_duration_ - 1)) / frame_duration_); - + + bool mode_matched = false; + // Get first avaliable video mode for Output + if (decklink_output_interface_->GetDisplayModeIterator(&display_mode_iterator) == + S_OK) { + while (display_mode_iterator->Next(&display_mode) == S_OK) { + if (display_mode->GetDisplayMode() == current_display_mode_) { + + mode_matched = true; + { + // get the name of the display mode, for display only + DECKLINK_STR modeName = nullptr; + display_mode->GetName(&modeName); + display_mode_name_ = decklink_string_to_std(modeName); + decklink_free_string(modeName); + } + + report_status( + fmt::format( + "Starting Decklink output loop in mode {}.", display_mode_name_), + false); + + frame_width_ = display_mode->GetWidth(); + frame_height_ = display_mode->GetHeight(); + display_mode->GetFrameRate(&frame_duration_, &frame_timescale_); + + uiFPS = ((frame_timescale_ + (frame_duration_ - 1)) / frame_duration_); + if (decklink_output_interface_->EnableVideoOutput( display_mode->GetDisplayMode(), bmdVideoOutputFlagDefault) != S_OK) { throw std::runtime_error("EnableVideoOutput call failed."); } + video_output_enabled_ = true; } } } - - if (!mode_matched) { - throw std::runtime_error("Failed to find display mode."); - } - - uiTotalFrames = 0; - - // Set the audio output mode + + if (!mode_matched) { + throw std::runtime_error("Failed to find display mode."); + } + + uiTotalFrames = 0; + + // Set the audio output mode if (decklink_output_interface_->EnableAudioOutput( bmdAudioSampleRate48kHz, bmdAudioSampleType16bitInteger, @@ -605,455 +619,477 @@ bool DecklinkOutput::start_sdi_output() { bmdAudioOutputStreamTimestamped) != S_OK) { throw std::runtime_error("Failed to enable audio output."); } + audio_output_enabled_ = true; set_preroll(); - - samples_delivered_ = 0; - + + samples_delivered_ = 0; + if (decklink_output_interface_->BeginAudioPreroll() != S_OK) { throw std::runtime_error("Failed to pre-roll audio output."); } decklink_output_interface_->StartScheduledPlayback(0, 100, 1.0); + scheduled_playback_started_ = true; bSuccess = true; } catch (std::exception &e) { - + if (scheduled_playback_started_) { + decklink_output_interface_->StopScheduledPlayback(0, NULL, 0); + scheduled_playback_started_ = false; + } + if (audio_output_enabled_) { + decklink_output_interface_->DisableAudioOutput(); + audio_output_enabled_ = false; + } + if (video_output_enabled_) { + decklink_output_interface_->DisableVideoOutput(); + video_output_enabled_ = false; + } report_error(e.what()); } - - if (display_mode) { - display_mode->Release(); - display_mode = NULL; - } - - if (display_mode_iterator) { - display_mode_iterator->Release(); - display_mode_iterator = NULL; - } - - return bSuccess; -} - -bool DecklinkOutput::stop_sdi_output(const std::string &error_message) { - - running_ = false; - decklink_xstudio_plugin_->stop(); - - if (!error_message.empty()) { - report_error(error_message); - } else { - report_status("SDI Output Paused.", false); - } - + + if (display_mode) { + display_mode->Release(); + display_mode = NULL; + } + + if (display_mode_iterator) { + display_mode_iterator->Release(); + display_mode_iterator = NULL; + } + + return bSuccess; +} + +bool DecklinkOutput::stop_sdi_output(const std::string &error_message) { + + running_ = false; + decklink_xstudio_plugin_->stop(); + + if (!error_message.empty()) { + report_error(error_message); + } else { + report_status("SDI Output Paused.", false); + } + spdlog::info("Stopping Decklink output loop. {}", error_message); if (decklink_output_interface_) { - decklink_output_interface_->StopScheduledPlayback(0, NULL, 0); - decklink_output_interface_->DisableVideoOutput(); - 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); - pFrameBuf = NULL; - - mutex_.unlock(); - - spdlog::info("Stopping Decklink output loop done. {}", error_message); - - return true; -} - -void DecklinkOutput::StartStop() { - if (!running_) - start_sdi_output(); - else - stop_sdi_output(); -} - -void DecklinkOutput::incoming_frame(const media_reader::ImageBufPtr &incoming) { - - // this is called from xstudio managed thread, which is independent of - // the decklink output thread control - frames_mutex_.lock(); - current_frame_ = incoming; - frames_mutex_.unlock(); -} - -namespace { -void multithreadMemCopy(void *_dst, void *_src, size_t buf_size, const int n_threads) { - - // Note: my instinct tells me that spawning threads for - // every copy operation (which might happen 60 times a second) - // is not efficient but it seems that having a threadpool doesn't - // make any real difference, the overhead of thread creation - // is tiny. - std::vector memcpy_threads; - size_t step = ((buf_size / n_threads) / 4096) * 4096; - - uint8_t *dst = (uint8_t *)_dst; - uint8_t *src = (uint8_t *)_src; - - for (int i = 0; i < n_threads; ++i) { - memcpy_threads.emplace_back(memcpy, dst, src, std::min(buf_size, step)); - dst += step; - src += step; - buf_size -= step; - } - - // ensure any threads still running to copy data to this texture are done - for (auto &t : memcpy_threads) { - if (t.joinable()) - t.join(); - } -} - -} // namespace - -#define CHECK_BIT(var, pos) ((var) & (1 << (pos))) - -void DecklinkOutput::report_status( - const std::string &status_message, const bool sdi_output_is_active) { - - utility::JsonStore j; - j["status_message"] = status_message; - j["sdi_output_is_active"] = sdi_output_is_active; - j["error_state"] = false; - decklink_xstudio_plugin_->send_status(j); -} - -void DecklinkOutput::report_error(const std::string &status_message) { - - utility::JsonStore j; - j["status_message"] = status_message; - j["sdi_output_is_active"] = false; - j["error_state"] = true; - decklink_xstudio_plugin_->send_status(j); -} - -void DecklinkOutput::fill_decklink_video_frame(IDeckLinkVideoFrame *decklink_video_frame) { - - // this function (fill_decklink_video_frame) is called by the Decklink API at a steady beat - // matching the refresh rate of the SDI output. We can therefore use it to tell our - // offscreen viewport to render a new frame ready for the subsequent call to this function. - // Remember The frame rendered by xstduio is delivered to us via the incoming_frame callback - // and, as long as xstudio is able to render the video frame in somthing less than the - // refresh period for the SDI output, it should have been delivered before we re-enter this - // function. - // - // The time value passed into this request is our best estimate of when the frame that we - // are requesting will actually be put on the screen. - decklink_xstudio_plugin_->request_video_frame(utility::clock::now()); - - - // We also need to make this crucial call to tell xstudio's offscreen viewport when the - // last video frame was put on screen. It uses the regular beat of these calls to work - // out the refresh rate of the video output and therefore do an accurate 'pulldown' when - // evaluating the playhead position for the next frame to go on screen. - // In the case of the Decklink, we know that this function (fill_decklink_video_frame) is - // being called with a beat matching the SDI refresh (as long as our code immediately below - // completes well inside/ that period) - decklink_xstudio_plugin_->video_frame_consumed(utility::clock::now()); - - static auto tp = utility::clock::now(); - auto tp1 = utility::clock::now(); - tp = tp1; - - mutex_.lock(); - - // SDK v15.3: GetBytes() moved from IDeckLinkVideoFrame to IDeckLinkVideoBuffer - IDeckLinkVideoBuffer *video_buffer = nullptr; - decklink_video_frame->QueryInterface(IID_IDeckLinkVideoBuffer, (void **)&video_buffer); - - frames_mutex_.lock(); - media_reader::ImageBufPtr the_frame = current_frame_; - frames_mutex_.unlock(); - - if (the_frame) { - - auto tp = utility::clock::now(); - - if (the_frame->size() >= decklink_video_frame->GetRowBytes() * frame_height_) { - - int xstudio_buf_pixel_format = the_frame->params().value("pixel_format", 0); - - if (xstudio_buf_pixel_format == ui::viewport::RGBA_16 && video_buffer) { - - // On macOS, DMA buffers may need StartAccess to map into CPU memory - HRESULT sa_hr = video_buffer->StartAccess(bmdBufferAccessReadAndWrite); - try { - - void *pFrame = nullptr; - HRESULT gb_hr = video_buffer->GetBytes((void **)&pFrame); - int num_pix = - decklink_video_frame->GetWidth() * decklink_video_frame->GetHeight(); - void *src_buf = the_frame->buffer(); - - // Validate pointers and ensure source buffer is large enough - // for num_pix RGBA_16 pixels (8 bytes each) - if (!pFrame || !src_buf) { - throw std::runtime_error( - fmt::format( - "Decklink: null buffer in fill_decklink_video_frame " - "(pFrame={}, src_buf={}, num_pix={}, StartAccess=0x{:x}, " - "GetBytes=0x{:x})", - pFrame, - src_buf, - num_pix, - (unsigned long)sa_hr, - (unsigned long)gb_hr)); - } else if (the_frame->size() < (size_t)num_pix * 8) { - throw std::runtime_error( - fmt::format( - "Decklink: source buffer too small " - "(size={}, need={}, frame={}x{}, src_format=RGBA_16)", - the_frame->size(), - (size_t)num_pix * 8, - decklink_video_frame->GetWidth(), - decklink_video_frame->GetHeight())); - } else if (decklink_video_frame->GetPixelFormat() == bmdFormat10BitRGB) { - - // TimeLogger l("RGBA16_to_10bitRGB"); - pixel_swizzler_.copy_frame_buffer_10bit( - pFrame, src_buf, num_pix); - - } else if (decklink_video_frame->GetPixelFormat() == bmdFormat10BitRGBXLE) { - - // TimeLogger l("RGBA16_to_10bitRGBXLE"); - pixel_swizzler_.copy_frame_buffer_10bit( - pFrame, src_buf, num_pix); - - } else if (decklink_video_frame->GetPixelFormat() == bmdFormat10BitRGBX) { - - // TimeLogger l("RGBA16_to_10bitRGBX"); - pixel_swizzler_.copy_frame_buffer_10bit( - pFrame, src_buf, num_pix); - - } else if (decklink_video_frame->GetPixelFormat() == bmdFormat12BitRGB) { - - // TimeLogger l("RGBA16_to_12bitRGB"); - pixel_swizzler_.copy_frame_buffer_12bit( - pFrame, src_buf, num_pix); - - } else if (decklink_video_frame->GetPixelFormat() == bmdFormat12BitRGBLE) { - - // TimeLogger l("RGBA16_to_12bitRGBLE"); - pixel_swizzler_.copy_frame_buffer_12bit( - 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"); - make_intermediate_frame(); - - IDeckLinkVideoBuffer *intermediate_video_buffer = nullptr; - auto r = intermediate_frame_->QueryInterface( - IID_IDeckLinkVideoBuffer, (void **)&intermediate_video_buffer); - if (r != S_OK || !intermediate_video_buffer) { - throw std::runtime_error("Failed to get conversion video buffer"); - } - - r = intermediate_video_buffer->StartAccess(bmdBufferAccessWrite); - if (r != S_OK) { - throw std::runtime_error( - "Could not access the video frame byte buffer"); - } - - void *pFrame2 = nullptr; - - r = intermediate_video_buffer->GetBytes((void **)&pFrame2); - if (r != S_OK || !pFrame2) { - throw std::runtime_error( - "Conversion video buffer has no bytes pointer"); - } - pixel_swizzler_.copy_frame_buffer_12bit( - pFrame2, src_buf, num_pix); - - intermediate_video_buffer->EndAccess(bmdBufferAccessWrite); - - // Now we use Decklink's conversion to convert from our intermediate 12 - // bit RGB frame to the desired output format (e.g. 10 bit YUV) - r = frame_converter_->ConvertFrame( - intermediate_frame_, decklink_video_frame); - if (r != S_OK) { - throw std::runtime_error("Failed to convert frame"); - } - } - - } catch (std::exception &e) { - - // reduce log spamming if we're getting errors on every frame - static int error_count = 0; - static std::string last_error; - if (last_error != e.what() || (++error_count) == 120) { - spdlog::error("{} {}", __PRETTY_FUNCTION__, e.what()); - last_error = e.what(); - error_count = 0; - } - } - video_buffer->EndAccess(bmdBufferAccessReadAndWrite); - } + if (scheduled_playback_started_) { + decklink_output_interface_->StopScheduledPlayback(0, NULL, 0); + scheduled_playback_started_ = false; } - } - - try { - - IDeckLinkMutableVideoFrame *mutable_video_buffer = nullptr; - auto r = decklink_video_frame->QueryInterface( - IID_IDeckLinkMutableVideoFrame, (void **)&mutable_video_buffer); - if (r != S_OK || !mutable_video_buffer) { - throw std::runtime_error("Failed to get mutable video buffer"); + if (video_output_enabled_) { + decklink_output_interface_->DisableVideoOutput(); + video_output_enabled_ = false; } - update_frame_metadata(mutable_video_buffer); - - } catch (std::exception &e) { - // reduce log spamming if we're getting errors on every frame - static int error_count = 0; - static std::string last_error; - if (last_error != e.what() || (++error_count) == 120) { - spdlog::error("{} {}", __PRETTY_FUNCTION__, e.what()); - last_error = e.what(); - error_count = 0; + if (audio_output_enabled_) { + decklink_output_interface_->DisableAudioOutput(); + audio_output_enabled_ = false; } } - if (video_buffer) - video_buffer->Release(); - - if (decklink_output_interface_->ScheduleVideoFrame( - decklink_video_frame, - (uiTotalFrames * frame_duration_), - frame_duration_, - frame_timescale_) != S_OK) { - mutex_.unlock(); - running_ = false; - decklink_xstudio_plugin_->stop(); - report_error("Failed to schedule video frame."); - return; - } - - if (!running_) { - running_ = true; - report_status(fmt::format("Running in mode {}.", display_mode_name_), running_); - decklink_xstudio_plugin_->start(frameWidth(), frameHeight()); - } - uiTotalFrames++; - mutex_.unlock(); -} - -void DecklinkOutput::update_frame_metadata(IDeckLinkMutableVideoFrame *mutableFrame) { - - if (hdr_metadata_.EOTF == 0) { - // SDR Mode - we don't need to set any metadata, but we do need to make sure to clear - // the HDR flag if it was set by a previous HDR frame - mutableFrame->SetFlags(mutableFrame->GetFlags() & ~bmdFrameContainsHDRMetadata); - return; - } - - mutableFrame->SetFlags(mutableFrame->GetFlags() | bmdFrameContainsHDRMetadata); - - IDeckLinkVideoFrameMutableMetadataExtensions *frameMeta = nullptr; - if (mutableFrame->QueryInterface( - IID_IDeckLinkVideoFrameMutableMetadataExtensions, (void **)&frameMeta) != S_OK) { - // This can fail if the drivers are old and don't support HDR metadata, in which case we - // just won't send any metadata - throw std::runtime_error( - "Failed to get mutable metadata extensions for HDR metadata update."); - } - - frameMeta->AddRef(); - frameMeta->SetInt(bmdDeckLinkFrameMetadataColorspace, hdr_metadata_.colourspace_); - frameMeta->SetInt( - bmdDeckLinkFrameMetadataHDRElectroOpticalTransferFunc, hdr_metadata_.EOTF); - frameMeta->SetFloat( - bmdDeckLinkFrameMetadataHDRDisplayPrimariesRedX, hdr_metadata_.referencePrimaries[0]); - frameMeta->SetFloat( - bmdDeckLinkFrameMetadataHDRDisplayPrimariesRedY, hdr_metadata_.referencePrimaries[1]); - frameMeta->SetFloat( - bmdDeckLinkFrameMetadataHDRDisplayPrimariesGreenX, hdr_metadata_.referencePrimaries[2]); - frameMeta->SetFloat( - bmdDeckLinkFrameMetadataHDRDisplayPrimariesGreenY, hdr_metadata_.referencePrimaries[3]); - frameMeta->SetFloat( - bmdDeckLinkFrameMetadataHDRDisplayPrimariesBlueX, hdr_metadata_.referencePrimaries[4]); - frameMeta->SetFloat( - bmdDeckLinkFrameMetadataHDRDisplayPrimariesBlueY, hdr_metadata_.referencePrimaries[5]); - frameMeta->SetFloat( - bmdDeckLinkFrameMetadataHDRWhitePointX, hdr_metadata_.referencePrimaries[6]); - frameMeta->SetFloat( - bmdDeckLinkFrameMetadataHDRWhitePointY, hdr_metadata_.referencePrimaries[7]); - frameMeta->SetFloat( - bmdDeckLinkFrameMetadataHDRMaxDisplayMasteringLuminance, - hdr_metadata_.luminanceSettings[0]); - frameMeta->SetFloat( - bmdDeckLinkFrameMetadataHDRMinDisplayMasteringLuminance, - hdr_metadata_.luminanceSettings[1]); - frameMeta->SetFloat( - bmdDeckLinkFrameMetadataHDRMaximumContentLightLevel, - hdr_metadata_.luminanceSettings[2]); - frameMeta->SetFloat( - bmdDeckLinkFrameMetadataHDRMaximumFrameAverageLightLevel, - hdr_metadata_.luminanceSettings[3]); - frameMeta->Release(); -} - -void DecklinkOutput::receive_samples_from_xstudio(int16_t *samples, unsigned long num_samps) { - // note this method is called by the xstudio audio output thread in a loop - // that streams chunks of samples to an audio output device (i.e. this class) - // The xstudio audio output actor expects us to return from here only when the - // samples have been 'consumed'. { - // lock mutex and immediately copy our samples to the buffer ready to - // send to Decklink - std::unique_lock lk2(audio_samples_buf_mutex_); - const size_t buf_size = audio_samples_buffer_.size(); - audio_samples_buffer_.resize(buf_size + num_samps); - memcpy(audio_samples_buffer_.data() + buf_size, samples, num_samps * sizeof(int16_t)); - } - - // now WAIT until the samples have been played (RenderAudioSamples is called - // from a Decklink driver thread - we only want to return from THIS function - // when Decklink needs a top-up of audio samples) - std::unique_lock lk(audio_samples_cv_mutex_); - audio_samples_cv_.wait(lk, [=] { return fetch_more_samples_from_xstudio_; }); - fetch_more_samples_from_xstudio_ = false; -} - -long DecklinkOutput::num_samples_in_buffer() { - - // note this method is called by the xstudio audio output thread - // Have to assume that GetBufferedAudioSampleFrameCount is not thread safe. BMD SDK - // does not tell us otherwise - if (!decklink_output_interface_) { - return 0; - } - std::unique_lock lk0(bmd_mutex_); - uint32_t prerollAudioSampleCount; - if (decklink_output_interface_->GetBufferedAudioSampleFrameCount( - &prerollAudioSampleCount) == S_OK) { - return (long)prerollAudioSampleCount - (audio_sync_delay_milliseconds_ * 48000) / 1000; + std::lock_guard lk(audio_samples_cv_mutex_); + fetch_more_samples_from_xstudio_ = true; } - return 0; -} - -// Note, I have not yet understood the significance of the preroll flag -void DecklinkOutput::copy_audio_samples_to_decklink_buffer(const bool /*preroll*/) { - + audio_samples_cv_.notify_all(); + + mutex_.lock(); + + free(pFrameBuf); + pFrameBuf = NULL; + + mutex_.unlock(); + + spdlog::info("Stopping Decklink output loop done. {}", error_message); + + return true; +} + +void DecklinkOutput::StartStop() { + if (!running_) + start_sdi_output(); + else + stop_sdi_output(); +} + +void DecklinkOutput::incoming_frame(const media_reader::ImageBufPtr &incoming) { + + // this is called from xstudio managed thread, which is independent of + // the decklink output thread control + frames_mutex_.lock(); + current_frame_ = incoming; + frames_mutex_.unlock(); +} + +namespace { +void multithreadMemCopy(void *_dst, void *_src, size_t buf_size, const int n_threads) { + + // Note: my instinct tells me that spawning threads for + // every copy operation (which might happen 60 times a second) + // is not efficient but it seems that having a threadpool doesn't + // make any real difference, the overhead of thread creation + // is tiny. + std::vector memcpy_threads; + size_t step = ((buf_size / n_threads) / 4096) * 4096; + + uint8_t *dst = (uint8_t *)_dst; + uint8_t *src = (uint8_t *)_src; + + for (int i = 0; i < n_threads; ++i) { + memcpy_threads.emplace_back(memcpy, dst, src, std::min(buf_size, step)); + dst += step; + src += step; + buf_size -= step; + } + + // ensure any threads still running to copy data to this texture are done + for (auto &t : memcpy_threads) { + if (t.joinable()) + t.join(); + } +} + +} // namespace + +#define CHECK_BIT(var, pos) ((var) & (1 << (pos))) + +void DecklinkOutput::report_status( + const std::string &status_message, const bool sdi_output_is_active) { + + utility::JsonStore j; + j["status_message"] = status_message; + j["sdi_output_is_active"] = sdi_output_is_active; + j["error_state"] = false; + decklink_xstudio_plugin_->send_status(j); +} + +void DecklinkOutput::report_error(const std::string &status_message) { + + utility::JsonStore j; + j["status_message"] = status_message; + j["sdi_output_is_active"] = false; + j["error_state"] = true; + decklink_xstudio_plugin_->send_status(j); +} + +void DecklinkOutput::fill_decklink_video_frame(IDeckLinkVideoFrame *decklink_video_frame) { + + // this function (fill_decklink_video_frame) is called by the Decklink API at a steady beat + // matching the refresh rate of the SDI output. We can therefore use it to tell our + // offscreen viewport to render a new frame ready for the subsequent call to this function. + // Remember The frame rendered by xstduio is delivered to us via the incoming_frame callback + // and, as long as xstudio is able to render the video frame in somthing less than the + // refresh period for the SDI output, it should have been delivered before we re-enter this + // function. + // + // The time value passed into this request is our best estimate of when the frame that we + // are requesting will actually be put on the screen. + decklink_xstudio_plugin_->request_video_frame(utility::clock::now()); + + + // We also need to make this crucial call to tell xstudio's offscreen viewport when the + // last video frame was put on screen. It uses the regular beat of these calls to work + // out the refresh rate of the video output and therefore do an accurate 'pulldown' when + // evaluating the playhead position for the next frame to go on screen. + // In the case of the Decklink, we know that this function (fill_decklink_video_frame) is + // being called with a beat matching the SDI refresh (as long as our code immediately below + // completes well inside/ that period) + decklink_xstudio_plugin_->video_frame_consumed(utility::clock::now()); + + static auto tp = utility::clock::now(); + auto tp1 = utility::clock::now(); + tp = tp1; + + mutex_.lock(); + + // SDK v15.3: GetBytes() moved from IDeckLinkVideoFrame to IDeckLinkVideoBuffer + IDeckLinkVideoBuffer *video_buffer = nullptr; + decklink_video_frame->QueryInterface(IID_IDeckLinkVideoBuffer, (void **)&video_buffer); + + frames_mutex_.lock(); + media_reader::ImageBufPtr the_frame = current_frame_; + frames_mutex_.unlock(); + + if (the_frame) { + + auto tp = utility::clock::now(); + + if (the_frame->size() >= decklink_video_frame->GetRowBytes() * frame_height_) { + + int xstudio_buf_pixel_format = the_frame->params().value("pixel_format", 0); + + if (xstudio_buf_pixel_format == ui::viewport::RGBA_16 && video_buffer) { + + // On macOS, DMA buffers may need StartAccess to map into CPU memory + HRESULT sa_hr = video_buffer->StartAccess(bmdBufferAccessReadAndWrite); + try { + + void *pFrame = nullptr; + HRESULT gb_hr = video_buffer->GetBytes((void **)&pFrame); + int num_pix = + decklink_video_frame->GetWidth() * decklink_video_frame->GetHeight(); + void *src_buf = the_frame->buffer(); + + // Validate pointers and ensure source buffer is large enough + // for num_pix RGBA_16 pixels (8 bytes each) + if (!pFrame || !src_buf) { + throw std::runtime_error( + fmt::format( + "Decklink: null buffer in fill_decklink_video_frame " + "(pFrame={}, src_buf={}, num_pix={}, StartAccess=0x{:x}, " + "GetBytes=0x{:x})", + pFrame, + src_buf, + num_pix, + (unsigned long)sa_hr, + (unsigned long)gb_hr)); + } else if (the_frame->size() < (size_t)num_pix * 8) { + throw std::runtime_error( + fmt::format( + "Decklink: source buffer too small " + "(size={}, need={}, frame={}x{}, src_format=RGBA_16)", + the_frame->size(), + (size_t)num_pix * 8, + decklink_video_frame->GetWidth(), + decklink_video_frame->GetHeight())); + } else if (decklink_video_frame->GetPixelFormat() == bmdFormat10BitRGB) { + + // TimeLogger l("RGBA16_to_10bitRGB"); + pixel_swizzler_.copy_frame_buffer_10bit( + pFrame, src_buf, num_pix); + + } else if (decklink_video_frame->GetPixelFormat() == bmdFormat10BitRGBXLE) { + + // TimeLogger l("RGBA16_to_10bitRGBXLE"); + pixel_swizzler_.copy_frame_buffer_10bit( + pFrame, src_buf, num_pix); + + } else if (decklink_video_frame->GetPixelFormat() == bmdFormat10BitRGBX) { + + // TimeLogger l("RGBA16_to_10bitRGBX"); + pixel_swizzler_.copy_frame_buffer_10bit( + pFrame, src_buf, num_pix); + + } else if (decklink_video_frame->GetPixelFormat() == bmdFormat12BitRGB) { + + // TimeLogger l("RGBA16_to_12bitRGB"); + pixel_swizzler_.copy_frame_buffer_12bit( + pFrame, src_buf, num_pix); + + } else if (decklink_video_frame->GetPixelFormat() == bmdFormat12BitRGBLE) { + + // TimeLogger l("RGBA16_to_12bitRGBLE"); + pixel_swizzler_.copy_frame_buffer_12bit( + 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"); + make_intermediate_frame(); + + IDeckLinkVideoBuffer *intermediate_video_buffer = nullptr; + auto r = intermediate_frame_->QueryInterface( + IID_IDeckLinkVideoBuffer, (void **)&intermediate_video_buffer); + if (r != S_OK || !intermediate_video_buffer) { + throw std::runtime_error("Failed to get conversion video buffer"); + } + + r = intermediate_video_buffer->StartAccess(bmdBufferAccessWrite); + if (r != S_OK) { + throw std::runtime_error( + "Could not access the video frame byte buffer"); + } + + void *pFrame2 = nullptr; + + r = intermediate_video_buffer->GetBytes((void **)&pFrame2); + if (r != S_OK || !pFrame2) { + throw std::runtime_error( + "Conversion video buffer has no bytes pointer"); + } + pixel_swizzler_.copy_frame_buffer_12bit( + pFrame2, src_buf, num_pix); + + intermediate_video_buffer->EndAccess(bmdBufferAccessWrite); + + // Now we use Decklink's conversion to convert from our intermediate 12 + // bit RGB frame to the desired output format (e.g. 10 bit YUV) + r = frame_converter_->ConvertFrame( + intermediate_frame_, decklink_video_frame); + if (r != S_OK) { + throw std::runtime_error("Failed to convert frame"); + } + } + + } catch (std::exception &e) { + + // reduce log spamming if we're getting errors on every frame + static int error_count = 0; + static std::string last_error; + if (last_error != e.what() || (++error_count) == 120) { + spdlog::error("{} {}", __PRETTY_FUNCTION__, e.what()); + last_error = e.what(); + error_count = 0; + } + } + video_buffer->EndAccess(bmdBufferAccessReadAndWrite); + } + } + } + + try { + + IDeckLinkMutableVideoFrame *mutable_video_buffer = nullptr; + auto r = decklink_video_frame->QueryInterface( + IID_IDeckLinkMutableVideoFrame, (void **)&mutable_video_buffer); + if (r != S_OK || !mutable_video_buffer) { + throw std::runtime_error("Failed to get mutable video buffer"); + } + update_frame_metadata(mutable_video_buffer); + + } catch (std::exception &e) { + // reduce log spamming if we're getting errors on every frame + static int error_count = 0; + static std::string last_error; + if (last_error != e.what() || (++error_count) == 120) { + spdlog::error("{} {}", __PRETTY_FUNCTION__, e.what()); + last_error = e.what(); + error_count = 0; + } + } + + if (video_buffer) + video_buffer->Release(); + + if (decklink_output_interface_->ScheduleVideoFrame( + decklink_video_frame, + (uiTotalFrames * frame_duration_), + frame_duration_, + frame_timescale_) != S_OK) { + mutex_.unlock(); + running_ = false; + decklink_xstudio_plugin_->stop(); + report_error("Failed to schedule video frame."); + return; + } + + if (!running_) { + running_ = true; + report_status(fmt::format("Running in mode {}.", display_mode_name_), running_); + decklink_xstudio_plugin_->start(frameWidth(), frameHeight()); + } + uiTotalFrames++; + mutex_.unlock(); +} + +void DecklinkOutput::update_frame_metadata(IDeckLinkMutableVideoFrame *mutableFrame) { + + if (hdr_metadata_.EOTF == 0) { + // SDR Mode - we don't need to set any metadata, but we do need to make sure to clear + // the HDR flag if it was set by a previous HDR frame + mutableFrame->SetFlags(mutableFrame->GetFlags() & ~bmdFrameContainsHDRMetadata); + return; + } + + mutableFrame->SetFlags(mutableFrame->GetFlags() | bmdFrameContainsHDRMetadata); + + IDeckLinkVideoFrameMutableMetadataExtensions *frameMeta = nullptr; + if (mutableFrame->QueryInterface( + IID_IDeckLinkVideoFrameMutableMetadataExtensions, (void **)&frameMeta) != S_OK) { + // This can fail if the drivers are old and don't support HDR metadata, in which case we + // just won't send any metadata + throw std::runtime_error( + "Failed to get mutable metadata extensions for HDR metadata update."); + } + + frameMeta->AddRef(); + frameMeta->SetInt(bmdDeckLinkFrameMetadataColorspace, hdr_metadata_.colourspace_); + frameMeta->SetInt( + bmdDeckLinkFrameMetadataHDRElectroOpticalTransferFunc, hdr_metadata_.EOTF); + frameMeta->SetFloat( + bmdDeckLinkFrameMetadataHDRDisplayPrimariesRedX, hdr_metadata_.referencePrimaries[0]); + frameMeta->SetFloat( + bmdDeckLinkFrameMetadataHDRDisplayPrimariesRedY, hdr_metadata_.referencePrimaries[1]); + frameMeta->SetFloat( + bmdDeckLinkFrameMetadataHDRDisplayPrimariesGreenX, hdr_metadata_.referencePrimaries[2]); + frameMeta->SetFloat( + bmdDeckLinkFrameMetadataHDRDisplayPrimariesGreenY, hdr_metadata_.referencePrimaries[3]); + frameMeta->SetFloat( + bmdDeckLinkFrameMetadataHDRDisplayPrimariesBlueX, hdr_metadata_.referencePrimaries[4]); + frameMeta->SetFloat( + bmdDeckLinkFrameMetadataHDRDisplayPrimariesBlueY, hdr_metadata_.referencePrimaries[5]); + frameMeta->SetFloat( + bmdDeckLinkFrameMetadataHDRWhitePointX, hdr_metadata_.referencePrimaries[6]); + frameMeta->SetFloat( + bmdDeckLinkFrameMetadataHDRWhitePointY, hdr_metadata_.referencePrimaries[7]); + frameMeta->SetFloat( + bmdDeckLinkFrameMetadataHDRMaxDisplayMasteringLuminance, + hdr_metadata_.luminanceSettings[0]); + frameMeta->SetFloat( + bmdDeckLinkFrameMetadataHDRMinDisplayMasteringLuminance, + hdr_metadata_.luminanceSettings[1]); + frameMeta->SetFloat( + bmdDeckLinkFrameMetadataHDRMaximumContentLightLevel, + hdr_metadata_.luminanceSettings[2]); + frameMeta->SetFloat( + bmdDeckLinkFrameMetadataHDRMaximumFrameAverageLightLevel, + hdr_metadata_.luminanceSettings[3]); + frameMeta->Release(); +} + +void DecklinkOutput::receive_samples_from_xstudio(int16_t *samples, unsigned long num_samps) { + // note this method is called by the xstudio audio output thread in a loop + // that streams chunks of samples to an audio output device (i.e. this class) + // The xstudio audio output actor expects us to return from here only when the + // samples have been 'consumed'. + { + // lock mutex and immediately copy our samples to the buffer ready to + // send to Decklink + std::unique_lock lk2(audio_samples_buf_mutex_); + const size_t buf_size = audio_samples_buffer_.size(); + audio_samples_buffer_.resize(buf_size + num_samps); + memcpy(audio_samples_buffer_.data() + buf_size, samples, num_samps * sizeof(int16_t)); + } + + // now WAIT until the samples have been played (RenderAudioSamples is called + // from a Decklink driver thread - we only want to return from THIS function + // when Decklink needs a top-up of audio samples) + std::unique_lock lk(audio_samples_cv_mutex_); + audio_samples_cv_.wait(lk, [=] { return fetch_more_samples_from_xstudio_; }); + fetch_more_samples_from_xstudio_ = false; +} + +long DecklinkOutput::num_samples_in_buffer() { + + // note this method is called by the xstudio audio output thread + // Have to assume that GetBufferedAudioSampleFrameCount is not thread safe. BMD SDK + // does not tell us otherwise + if (!decklink_output_interface_) { + return 0; + } + std::unique_lock lk0(bmd_mutex_); + uint32_t prerollAudioSampleCount; + if (decklink_output_interface_->GetBufferedAudioSampleFrameCount( + &prerollAudioSampleCount) == S_OK) { + return (long)prerollAudioSampleCount - (audio_sync_delay_milliseconds_ * 48000) / 1000; + } + return 0; +} + +// Note, I have not yet understood the significance of the preroll flag +void DecklinkOutput::copy_audio_samples_to_decklink_buffer(const bool /*preroll*/) { + if (!decklink_output_interface_) { { std::lock_guard m(audio_samples_cv_mutex_); @@ -1062,97 +1098,113 @@ void DecklinkOutput::copy_audio_samples_to_decklink_buffer(const bool /*preroll* audio_samples_cv_.notify_one(); return; } - - std::unique_lock lk0(bmd_mutex_); - - // How many samples are sitting on the SDI card ready to be played? - uint32_t prerollAudioSampleCount; - if (decklink_output_interface_->GetBufferedAudioSampleFrameCount( - &prerollAudioSampleCount) == S_OK) { - if (prerollAudioSampleCount > samples_water_level_) { - // plenty of samples already in the bmd buffer ready to be played, - // let's do nothing here - return; - } else { - // We need to top-up the samples in the buffer. - - // the xstudio audio output thread is probably waiting in - // receive_samples_from_xstudio ... because the number of samples - // in the BMD buffer is below our target 'water_level' we now - // release the lock in 'receive_samples_from_xstudio' so that the - // xstudio audio sample streaming loop can continue and fetch - // more samples to give to us - { - std::lock_guard m(audio_samples_cv_mutex_); - fetch_more_samples_from_xstudio_ = true; - } - audio_samples_cv_.notify_one(); - } - } - - std::unique_lock lk(audio_samples_buf_mutex_); - - if (audio_samples_buffer_.empty()) { - // 512 samples of silence to start filling buffer in the absence - // of audio samples streaming from xstudio - audio_samples_buffer_.resize(4096); - memset( - audio_samples_buffer_.data(), 0, audio_samples_buffer_.size() * sizeof(uint16_t)); - } - - if (decklink_output_interface_->ScheduleAudioSamples( - audio_samples_buffer_.data(), - audio_samples_buffer_.size() / 2, - samples_delivered_, - bmdAudioSampleRate48kHz, - nullptr) != S_OK) { - throw std::runtime_error("Failed to shedule audio out."); + + std::unique_lock lk0(bmd_mutex_); + + // How many samples are sitting on the SDI card ready to be played? + uint32_t prerollAudioSampleCount; + if (decklink_output_interface_->GetBufferedAudioSampleFrameCount( + &prerollAudioSampleCount) == S_OK) { + if (prerollAudioSampleCount > samples_water_level_) { + // plenty of samples already in the bmd buffer ready to be played, + // let's do nothing here + return; + } else { + // We need to top-up the samples in the buffer. + + // the xstudio audio output thread is probably waiting in + // receive_samples_from_xstudio ... because the number of samples + // in the BMD buffer is below our target 'water_level' we now + // release the lock in 'receive_samples_from_xstudio' so that the + // xstudio audio sample streaming loop can continue and fetch + // more samples to give to us + { + std::lock_guard m(audio_samples_cv_mutex_); + fetch_more_samples_from_xstudio_ = true; + } + audio_samples_cv_.notify_one(); + } + } + + std::unique_lock lk(audio_samples_buf_mutex_); + + if (audio_samples_buffer_.empty()) { + // 512 samples of silence to start filling buffer in the absence + // of audio samples streaming from xstudio + audio_samples_buffer_.resize(4096); + memset( + audio_samples_buffer_.data(), 0, audio_samples_buffer_.size() * sizeof(uint16_t)); + } + + if (decklink_output_interface_->ScheduleAudioSamples( + audio_samples_buffer_.data(), + audio_samples_buffer_.size() / 2, + samples_delivered_, + bmdAudioSampleRate48kHz, + nullptr) != S_OK) { + throw std::runtime_error("Failed to shedule audio out."); + } + + samples_delivered_ += audio_samples_buffer_.size() / 2; + audio_samples_buffer_.clear(); +} + +//////////////////////////////////////////// +// Render Delegate Class +//////////////////////////////////////////// +AVOutputCallback::AVOutputCallback(DecklinkOutput *pOwner) { owner_ = pOwner; } + +HRESULT AVOutputCallback::QueryInterface(REFIID /*iid*/, LPVOID *ppv) { + if (!ppv) { + return E_INVALIDARG; } - samples_delivered_ += audio_samples_buffer_.size() / 2; - audio_samples_buffer_.clear(); -} - -//////////////////////////////////////////// -// Render Delegate Class -//////////////////////////////////////////// -AVOutputCallback::AVOutputCallback(DecklinkOutput *pOwner) { owner_ = pOwner; } - -HRESULT AVOutputCallback::QueryInterface(REFIID /*iid*/, LPVOID *ppv) { *ppv = NULL; - return E_NOINTERFACE; -} - -ULONG AVOutputCallback::AddRef() { - int oldValue; - oldValue = ref_count_.fetchAndAddAcquire(1); - return (ULONG)(oldValue + 1); -} - -ULONG AVOutputCallback::Release() { - int oldValue; - - oldValue = ref_count_.fetchAndAddAcquire(-1); - if (oldValue == 1) { - delete this; + if (std::memcmp(&iid, &IID_IUnknown, sizeof(REFIID)) == 0) { + *ppv = static_cast(static_cast(this)); + } else if (std::memcmp(&iid, &IID_IDeckLinkVideoOutputCallback, sizeof(REFIID)) == 0) { + *ppv = static_cast(this); + } else if (std::memcmp(&iid, &IID_IDeckLinkAudioOutputCallback, sizeof(REFIID)) == 0) { + *ppv = static_cast(this); + } else { + return E_NOINTERFACE; } - return (ULONG)(oldValue - 1); -} - -HRESULT AVOutputCallback::ScheduledFrameCompleted( - IDeckLinkVideoFrame *completedFrame, BMDOutputFrameCompletionResult /*result*/) { - owner_->fill_decklink_video_frame(completedFrame); - return S_OK; -} - -HRESULT AVOutputCallback::ScheduledPlaybackHasStopped() { return S_OK; } - -HRESULT AVOutputCallback::RenderAudioSamples(BOOL preroll) { - // decklink driver is calling this at regular intervals. There may be - // plenty of samples in the buffer for it to render, we check that in - // our own function - owner_->copy_audio_samples_to_decklink_buffer(preroll); + AddRef(); return S_OK; } + +ULONG AVOutputCallback::AddRef() { + int oldValue; + + oldValue = ref_count_.fetchAndAddAcquire(1); + return (ULONG)(oldValue + 1); +} + +ULONG AVOutputCallback::Release() { + int oldValue; + + oldValue = ref_count_.fetchAndAddAcquire(-1); + if (oldValue == 1) { + delete this; + } + + return (ULONG)(oldValue - 1); +} + +HRESULT AVOutputCallback::ScheduledFrameCompleted( + IDeckLinkVideoFrame *completedFrame, BMDOutputFrameCompletionResult /*result*/) { + owner_->fill_decklink_video_frame(completedFrame); + return S_OK; +} + +HRESULT AVOutputCallback::ScheduledPlaybackHasStopped() { return S_OK; } + +HRESULT AVOutputCallback::RenderAudioSamples(BOOL preroll) { + // decklink driver is calling this at regular intervals. There may be + // plenty of samples in the buffer for it to render, we check that in + // our own function + owner_->copy_audio_samples_to_decklink_buffer(preroll); + return S_OK; +} 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 9952576e3..0cfad5e69 100644 --- a/src/plugin/video_output/bmd_decklink/src/decklink_output.hpp +++ b/src/plugin/video_output/bmd_decklink/src/decklink_output.hpp @@ -1,250 +1,253 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -#ifdef __APPLE__ -#include -#elif defined(_WIN32) -#ifndef NOMINMAX -#define NOMINMAX -#endif -#ifndef WIN32_LEAN_AND_MEAN -#define WIN32_LEAN_AND_MEAN -#endif -#include -#include -#else -#include -#endif -#include -#include -#include -#include -#include - -#include "extern/decklink_compat.h" -#include "extern/DeckLinkAPI.h" - -#ifndef STDMETHODCALLTYPE -#define STDMETHODCALLTYPE -#endif -#include "xstudio/media_reader/image_buffer.hpp" -#include "pixel_swizzler.hpp" - -namespace xstudio { -namespace bm_decklink_plugin_1_0 { - - class AVOutputCallback; - - struct HDRMetadata { - int64_t EOTF = {0}; - int32_t colourspace_ = {bmdColorspaceRec709}; - std::array referencePrimaries = { - 0.708, 0.292, 0.170, 0.797, 0.131, 0.046, 0.3127, 0.3290}; - std::array luminanceSettings = { - 1000.0, // HDRMaxDisplayMasteringLuminance - 0.0005, // HDRMinDisplayMasteringLuminance - 1000.0, // HDRMaximumContentLightLevel - 400.0 // HDRMaximumFrameAverageLightLevel - }; - }; - - class BMDecklinkPlugin; - - class MockDecklinkOutput { - - public: - MockDecklinkOutput(BMDecklinkPlugin *decklink_xstudio_plugin) {} - - bool init_decklink() { return true; } - - bool start_sdi_output() { return true; } - void set_preroll() {} - bool stop_sdi_output(const std::string &error = std::string()) { return true; } - void StartStop() {} - - void fill_decklink_video_frame(IDeckLinkVideoFrame *decklink_video_frame) {} - void copy_audio_samples_to_decklink_buffer(const bool preroll) {} - void receive_samples_from_xstudio(int16_t *samples, unsigned long num_samps) {} - long num_samples_in_buffer() { return 0; } - void set_display_mode( - const std::string &resolution, - const std::string &refresh_rate, - const BMDPixelFormat pix_format) {} - void set_audio_samples_water_level(const int w) {} - void set_audio_sync_delay_milliseconds(const long ms_delay) {} - - void incoming_frame(const media_reader::ImageBufPtr &frame) {} - - [[nodiscard]] int frameWidth() const { return static_cast(1920); } - [[nodiscard]] int frameHeight() const { return static_cast(1080); } - - std::vector - get_available_refresh_rates(const std::string &output_resolution) const { - return std::vector( - {"23.976", "24", "25", "29.97", "30", "50", "59.94", "60"}); - } - - std::vector output_resolution_names() const { - return std::vector({"1920x1080", "3840x2160"}); - } - - void set_hdr_metadata(const HDRMetadata &) {} - }; - - class DecklinkOutput { - - public: - DecklinkOutput(BMDecklinkPlugin *decklink_xstudio_plugin); - ~DecklinkOutput(); - - static void check_decklink_installation(); - - bool init_decklink(); - void make_intermediate_frame(); - bool start_sdi_output(); - void set_preroll(); - bool stop_sdi_output(const std::string &error = std::string()); - void StartStop(); - - void fill_decklink_video_frame(IDeckLinkVideoFrame *decklink_video_frame); - void update_frame_metadata(IDeckLinkMutableVideoFrame *displayFrame); - void copy_audio_samples_to_decklink_buffer(const bool preroll); - void receive_samples_from_xstudio(int16_t *samples, unsigned long num_samps); - long num_samples_in_buffer(); - void set_display_mode( - const std::string &resolution, - const std::string &refresh_rate, - const BMDPixelFormat pix_format); - void set_audio_samples_water_level(const int w) { samples_water_level_ = (uint32_t)w; } - void set_audio_sync_delay_milliseconds(const long ms_delay) { - audio_sync_delay_milliseconds_ = ms_delay; - } - - void incoming_frame(const media_reader::ImageBufPtr &frame); - - [[nodiscard]] int frameWidth() const { return static_cast(frame_width_); } - [[nodiscard]] int frameHeight() const { return static_cast(frame_height_); } - - std::vector - get_available_refresh_rates(const std::string &output_resolution) const; - - std::vector output_resolution_names() const { - std::vector result; - for (const auto &p : refresh_rate_per_output_resolution_) { - result.push_back(p.first); - } - std::sort(result.begin(), result.end()); - return result; - } - - void set_hdr_metadata(const HDRMetadata &o) { - hdr_metadata_mutex_.lock(); - hdr_metadata_ = o; - hdr_metadata_mutex_.unlock(); - } - +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#ifdef __APPLE__ +#include +#elif defined(_WIN32) +#ifndef NOMINMAX +#define NOMINMAX +#endif +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#include +#else +#include +#endif +#include +#include +#include +#include +#include + +#include "extern/decklink_compat.h" +#include "extern/DeckLinkAPI.h" + +#ifndef STDMETHODCALLTYPE +#define STDMETHODCALLTYPE +#endif +#include "xstudio/media_reader/image_buffer.hpp" +#include "pixel_swizzler.hpp" + +namespace xstudio { +namespace bm_decklink_plugin_1_0 { + + class AVOutputCallback; + + struct HDRMetadata { + int64_t EOTF = {0}; + int32_t colourspace_ = {bmdColorspaceRec709}; + std::array referencePrimaries = { + 0.708, 0.292, 0.170, 0.797, 0.131, 0.046, 0.3127, 0.3290}; + std::array luminanceSettings = { + 1000.0, // HDRMaxDisplayMasteringLuminance + 0.0005, // HDRMinDisplayMasteringLuminance + 1000.0, // HDRMaximumContentLightLevel + 400.0 // HDRMaximumFrameAverageLightLevel + }; + }; + + class BMDecklinkPlugin; + + class MockDecklinkOutput { + + public: + MockDecklinkOutput(BMDecklinkPlugin *decklink_xstudio_plugin) {} + + bool init_decklink() { return true; } + + bool start_sdi_output() { return true; } + void set_preroll() {} + bool stop_sdi_output(const std::string &error = std::string()) { return true; } + void StartStop() {} + + void fill_decklink_video_frame(IDeckLinkVideoFrame *decklink_video_frame) {} + void copy_audio_samples_to_decklink_buffer(const bool preroll) {} + void receive_samples_from_xstudio(int16_t *samples, unsigned long num_samps) {} + long num_samples_in_buffer() { return 0; } + void set_display_mode( + const std::string &resolution, + const std::string &refresh_rate, + const BMDPixelFormat pix_format) {} + void set_audio_samples_water_level(const int w) {} + void set_audio_sync_delay_milliseconds(const long ms_delay) {} + + void incoming_frame(const media_reader::ImageBufPtr &frame) {} + + [[nodiscard]] int frameWidth() const { return static_cast(1920); } + [[nodiscard]] int frameHeight() const { return static_cast(1080); } + + std::vector + get_available_refresh_rates(const std::string &output_resolution) const { + return std::vector( + {"23.976", "24", "25", "29.97", "30", "50", "59.94", "60"}); + } + + std::vector output_resolution_names() const { + return std::vector({"1920x1080", "3840x2160"}); + } + + void set_hdr_metadata(const HDRMetadata &) {} + }; + + class DecklinkOutput { + + public: + DecklinkOutput(BMDecklinkPlugin *decklink_xstudio_plugin); + ~DecklinkOutput(); + + static void check_decklink_installation(); + + bool init_decklink(); + void make_intermediate_frame(); + bool start_sdi_output(); + void set_preroll(); + bool stop_sdi_output(const std::string &error = std::string()); + void StartStop(); + + void fill_decklink_video_frame(IDeckLinkVideoFrame *decklink_video_frame); + void update_frame_metadata(IDeckLinkMutableVideoFrame *displayFrame); + void copy_audio_samples_to_decklink_buffer(const bool preroll); + void receive_samples_from_xstudio(int16_t *samples, unsigned long num_samps); + long num_samples_in_buffer(); + void set_display_mode( + const std::string &resolution, + const std::string &refresh_rate, + const BMDPixelFormat pix_format); + void set_audio_samples_water_level(const int w) { samples_water_level_ = (uint32_t)w; } + void set_audio_sync_delay_milliseconds(const long ms_delay) { + audio_sync_delay_milliseconds_ = ms_delay; + } + + void incoming_frame(const media_reader::ImageBufPtr &frame); + + [[nodiscard]] int frameWidth() const { return static_cast(frame_width_); } + [[nodiscard]] int frameHeight() const { return static_cast(frame_height_); } + + std::vector + get_available_refresh_rates(const std::string &output_resolution) const; + + std::vector output_resolution_names() const { + std::vector result; + for (const auto &p : refresh_rate_per_output_resolution_) { + result.push_back(p.first); + } + std::sort(result.begin(), result.end()); + return result; + } + + void set_hdr_metadata(const HDRMetadata &o) { + hdr_metadata_mutex_.lock(); + hdr_metadata_ = o; + hdr_metadata_mutex_.unlock(); + } + [[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: - void release_resources(); - void detect_runtime_info(); - void log_runtime_info() const; - - AVOutputCallback *output_callback_ = {nullptr}; - std::mutex mutex_; - - GLenum glStatus = {0}; - GLuint idFrameBuf = {0}, idColorBuf = {0}, idDepthBuf = {0}; - char *pFrameBuf = {nullptr}; - - // DeckLink - uint32_t frame_width_ = {0}; - uint32_t frame_height_ = {0}; - - IDeckLink *decklink_interface_ = {nullptr}; - IDeckLinkOutput *decklink_output_interface_ = {nullptr}; - IDeckLinkVideoConversion *frame_converter_ = {nullptr}; - - 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_; - bool running_ = {false}; - - void query_display_modes(); - - void report_status(const std::string &status_message, bool is_running); - - void report_error(const std::string &status_message); - - std::map> refresh_rate_per_output_resolution_; - std::map, BMDDisplayMode> display_modes_; - - BMDPixelFormat current_pix_format_; - BMDDisplayMode current_display_mode_; - std::string display_mode_name_; - - IDeckLinkMutableVideoFrame *intermediate_frame_ = {nullptr}; - - BMDecklinkPlugin *decklink_xstudio_plugin_; - - std::vector audio_samples_buffer_; - std::mutex audio_samples_buf_mutex_, audio_samples_cv_mutex_, bmd_mutex_; + + private: + void release_resources(); + void detect_runtime_info(); + void log_runtime_info() const; + + AVOutputCallback *output_callback_ = {nullptr}; + std::mutex mutex_; + + GLenum glStatus = {0}; + GLuint idFrameBuf = {0}, idColorBuf = {0}, idDepthBuf = {0}; + char *pFrameBuf = {nullptr}; + + // DeckLink + uint32_t frame_width_ = {0}; + uint32_t frame_height_ = {0}; + + IDeckLink *decklink_interface_ = {nullptr}; + IDeckLinkOutput *decklink_output_interface_ = {nullptr}; + IDeckLinkVideoConversion *frame_converter_ = {nullptr}; + + 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_; + bool running_ = {false}; + + void query_display_modes(); + + void report_status(const std::string &status_message, bool is_running); + + void report_error(const std::string &status_message); + + std::map> refresh_rate_per_output_resolution_; + std::map, BMDDisplayMode> display_modes_; + + BMDPixelFormat current_pix_format_; + BMDDisplayMode current_display_mode_; + std::string display_mode_name_; + + IDeckLinkMutableVideoFrame *intermediate_frame_ = {nullptr}; + + BMDecklinkPlugin *decklink_xstudio_plugin_; + + std::vector audio_samples_buffer_; + std::mutex audio_samples_buf_mutex_, audio_samples_cv_mutex_, bmd_mutex_; std::condition_variable audio_samples_cv_; bool fetch_more_samples_from_xstudio_ = {false}; unsigned long samples_delivered_ = {0}; uint32_t samples_water_level_ = {4096}; long audio_sync_delay_milliseconds_ = {0}; + bool video_output_enabled_ = {false}; + bool audio_output_enabled_ = {false}; + bool scheduled_playback_started_ = {false}; PixelSwizzler pixel_swizzler_; - + HDRMetadata hdr_metadata_; std::mutex hdr_metadata_mutex_; bool is_available_ = {false}; std::string last_error_ = {}; std::string runtime_info_ = {}; std::string output_interface_info_ = {}; - }; - - class AVOutputCallback : public IDeckLinkVideoOutputCallback, - public IDeckLinkAudioOutputCallback { - private: - struct RefCt { - - int fetchAndAddAcquire(const int delta) { - - std::lock_guard l(m); - int old = count; - count += delta; - return old; - } - std::atomic count = 1; - std::mutex m; - } ref_count_; - - DecklinkOutput *owner_; - - public: - AVOutputCallback(DecklinkOutput *pOwner); - - // IUnknown - HRESULT QueryInterface(REFIID /*iid*/, LPVOID * /*ppv*/) override; - ULONG AddRef() override; - ULONG Release() override; - - // IDeckLinkAudioOutputCallback - HRESULT RenderAudioSamples(BOOL preroll) override; - - // IDeckLinkVideoOutputCallback - HRESULT ScheduledFrameCompleted( - IDeckLinkVideoFrame *completedFrame, - BMDOutputFrameCompletionResult result) override; - HRESULT ScheduledPlaybackHasStopped() override; - }; - -} // namespace bm_decklink_plugin_1_0 -} // namespace xstudio + }; + + class AVOutputCallback : public IDeckLinkVideoOutputCallback, + public IDeckLinkAudioOutputCallback { + private: + struct RefCt { + + int fetchAndAddAcquire(const int delta) { + + std::lock_guard l(m); + int old = count; + count += delta; + return old; + } + std::atomic count = 1; + std::mutex m; + } ref_count_; + + DecklinkOutput *owner_; + + public: + AVOutputCallback(DecklinkOutput *pOwner); + + // IUnknown + HRESULT QueryInterface(REFIID /*iid*/, LPVOID * /*ppv*/) override; + ULONG AddRef() override; + ULONG Release() override; + + // IDeckLinkAudioOutputCallback + HRESULT RenderAudioSamples(BOOL preroll) override; + + // IDeckLinkVideoOutputCallback + HRESULT ScheduledFrameCompleted( + IDeckLinkVideoFrame *completedFrame, + BMDOutputFrameCompletionResult result) override; + HRESULT ScheduledPlaybackHasStopped() override; + }; + +} // namespace bm_decklink_plugin_1_0 +} // namespace xstudio From 90c6c4bf9eee20b45ddac40a6d10dbcb207d3509 Mon Sep 17 00:00:00 2001 From: "florence.beliveau" Date: Wed, 1 Apr 2026 18:16:37 +0200 Subject: [PATCH 04/12] Fix DeckLink QueryInterface parameter name --- src/plugin/video_output/bmd_decklink/src/decklink_output.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a1a8b866d..e67522f43 100644 --- a/src/plugin/video_output/bmd_decklink/src/decklink_output.cpp +++ b/src/plugin/video_output/bmd_decklink/src/decklink_output.cpp @@ -1154,7 +1154,7 @@ void DecklinkOutput::copy_audio_samples_to_decklink_buffer(const bool /*preroll* //////////////////////////////////////////// AVOutputCallback::AVOutputCallback(DecklinkOutput *pOwner) { owner_ = pOwner; } -HRESULT AVOutputCallback::QueryInterface(REFIID /*iid*/, LPVOID *ppv) { +HRESULT AVOutputCallback::QueryInterface(REFIID iid, LPVOID *ppv) { if (!ppv) { return E_INVALIDARG; } From b9dd0b5ec87e379a979095c2e2d55241e8f2add2 Mon Sep 17 00:00:00 2001 From: "florence.beliveau" Date: Thu, 2 Apr 2026 01:35:25 +0200 Subject: [PATCH 05/12] Fix DeckLink IUnknown comparison on Linux --- src/plugin/video_output/bmd_decklink/src/decklink_output.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 e67522f43..a88efa4e0 100644 --- a/src/plugin/video_output/bmd_decklink/src/decklink_output.cpp +++ b/src/plugin/video_output/bmd_decklink/src/decklink_output.cpp @@ -1161,7 +1161,9 @@ HRESULT AVOutputCallback::QueryInterface(REFIID iid, LPVOID *ppv) { *ppv = NULL; - if (std::memcmp(&iid, &IID_IUnknown, sizeof(REFIID)) == 0) { + const auto iid_unknown = IID_IUnknown; + + if (std::memcmp(&iid, &iid_unknown, sizeof(REFIID)) == 0) { *ppv = static_cast(static_cast(this)); } else if (std::memcmp(&iid, &IID_IDeckLinkVideoOutputCallback, sizeof(REFIID)) == 0) { *ppv = static_cast(this); From 00beffe173ed7a27ca323fbe3f83efbdd7e03647 Mon Sep 17 00:00:00 2001 From: "florence.beliveau" Date: Thu, 2 Apr 2026 02:23:25 +0200 Subject: [PATCH 06/12] Require modern DeckLink Linux runtime --- .../bmd_decklink/src/decklink_output.cpp | 69 +++++++++++-------- .../bmd_decklink/src/decklink_output.hpp | 24 +++---- 2 files changed, 52 insertions(+), 41 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 a88efa4e0..a5ec4b6fa 100644 --- a/src/plugin/video_output/bmd_decklink/src/decklink_output.cpp +++ b/src/plugin/video_output/bmd_decklink/src/decklink_output.cpp @@ -205,7 +205,6 @@ DecklinkOutput::~DecklinkOutput() { void DecklinkOutput::release_resources() { if (decklink_output_interface_ != NULL) { - spdlog::info("Stopping Decklink output loop."); if (scheduled_playback_started_) { @@ -220,12 +219,11 @@ void DecklinkOutput::release_resources() { decklink_output_interface_->DisableAudioOutput(); audio_output_enabled_ = false; } - decklink_output_interface_->Release(); } - if (decklink_interface_ != NULL) { - decklink_interface_->Release(); - } + if (decklink_interface_ != NULL) { + decklink_interface_->Release(); + } if (output_callback_ != NULL) { output_callback_->Release(); } @@ -348,8 +346,8 @@ bool DecklinkOutput::init_decklink() { bool bSuccess = false; IDeckLinkIterator *decklink_iterator = NULL; - last_error_.clear(); - output_interface_info_.clear(); + last_error_.clear(); + output_interface_info_.clear(); try { detect_runtime_info(); @@ -385,17 +383,26 @@ bool DecklinkOutput::init_decklink() { if (decklink_interface_->QueryInterface( IID_IDeckLinkOutput, (void **)&decklink_output_interface_) != S_OK) { #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 " + 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) { + output_interface_info_ = "IID_IDeckLinkOutput_v14_2_1"; + legacy_output_interface->Release(); + const auto upgrade_message = with_runtime_details( + "Unsupported legacy Blackmagic DeckLink Linux runtime detected " + "(output_interface=IID_IDeckLinkOutput_v14_2_1). Upgrade Blackmagic " + "Desktop Video drivers to a newer release to use Blackmagic cards in " + "xStudio.", + runtime_info_); + spdlog::error("{}", upgrade_message); + spdlog::error( + "Upgrade Blackmagic Desktop Video drivers to enable Blackmagic card " + "support on Linux."); + throw std::runtime_error(upgrade_message); + } else { + throw std::runtime_error(with_runtime_details( + "DeckLink runtime ABI mismatch: failed to query the video output " "interface", runtime_info_)); } @@ -412,12 +419,15 @@ bool DecklinkOutput::init_decklink() { if (output_callback_ == NULL) throw std::runtime_error("Failed to create Video Output Callback."); - if (decklink_output_interface_->SetScheduledFrameCompletionCallback(output_callback_) != - S_OK) - throw std::runtime_error("SetScheduledFrameCompletionCallback failed."); - - if (decklink_output_interface_->SetAudioCallback(output_callback_) != S_OK) - throw std::runtime_error("SetAudioCallback failed."); + if (decklink_output_interface_->SetScheduledFrameCompletionCallback( + static_cast(output_callback_)) != S_OK) { + throw std::runtime_error("SetScheduledFrameCompletionCallback failed."); + } + + if (decklink_output_interface_->SetAudioCallback( + static_cast(output_callback_)) != S_OK) { + throw std::runtime_error("SetAudioCallback failed."); + } #ifdef _WIN32 // Create an IDeckLinkVideoConversion interface object to provide pixel format @@ -445,11 +455,12 @@ bool DecklinkOutput::init_decklink() { #endif - bSuccess = true; - is_available_ = true; - log_runtime_info(); - - query_display_modes(); + bSuccess = true; + is_available_ = true; + log_runtime_info(); + spdlog::info("DeckLink runtime is supported."); + + query_display_modes(); } catch (std::exception &e) { 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 0cfad5e69..58128cd7c 100644 --- a/src/plugin/video_output/bmd_decklink/src/decklink_output.hpp +++ b/src/plugin/video_output/bmd_decklink/src/decklink_output.hpp @@ -21,8 +21,8 @@ #include #include -#include "extern/decklink_compat.h" -#include "extern/DeckLinkAPI.h" +#include "extern/decklink_compat.h" +#include "extern/DeckLinkAPI.h" #ifndef STDMETHODCALLTYPE #define STDMETHODCALLTYPE @@ -163,9 +163,9 @@ namespace bm_decklink_plugin_1_0 { uint32_t frame_width_ = {0}; uint32_t frame_height_ = {0}; - IDeckLink *decklink_interface_ = {nullptr}; - IDeckLinkOutput *decklink_output_interface_ = {nullptr}; - IDeckLinkVideoConversion *frame_converter_ = {nullptr}; + IDeckLink *decklink_interface_ = {nullptr}; + IDeckLinkOutput *decklink_output_interface_ = {nullptr}; + IDeckLinkVideoConversion *frame_converter_ = {nullptr}; BMDTimeValue frame_duration_ = {0}; BMDTimeScale frame_timescale_ = {0}; @@ -213,8 +213,8 @@ namespace bm_decklink_plugin_1_0 { std::string output_interface_info_ = {}; }; - class AVOutputCallback : public IDeckLinkVideoOutputCallback, - public IDeckLinkAudioOutputCallback { + class AVOutputCallback : public IDeckLinkVideoOutputCallback, + public IDeckLinkAudioOutputCallback { private: struct RefCt { @@ -243,11 +243,11 @@ namespace bm_decklink_plugin_1_0 { HRESULT RenderAudioSamples(BOOL preroll) override; // IDeckLinkVideoOutputCallback - HRESULT ScheduledFrameCompleted( - IDeckLinkVideoFrame *completedFrame, - BMDOutputFrameCompletionResult result) override; - HRESULT ScheduledPlaybackHasStopped() override; - }; + HRESULT ScheduledFrameCompleted( + IDeckLinkVideoFrame *completedFrame, + BMDOutputFrameCompletionResult result) override; + HRESULT ScheduledPlaybackHasStopped() override; + }; } // namespace bm_decklink_plugin_1_0 } // namespace xstudio From f3438768c1f758070091dc1fbea465e7d356fb78 Mon Sep 17 00:00:00 2001 From: "florence.beliveau" Date: Thu, 2 Apr 2026 02:34:20 +0200 Subject: [PATCH 07/12] Gate DeckLink Linux compatibility by driver version --- .../bmd_decklink/src/decklink_output.cpp | 108 +++++++++++++----- .../bmd_decklink/src/decklink_output.hpp | 3 +- 2 files changed, 79 insertions(+), 32 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 a5ec4b6fa..678b35965 100644 --- a/src/plugin/video_output/bmd_decklink/src/decklink_output.cpp +++ b/src/plugin/video_output/bmd_decklink/src/decklink_output.cpp @@ -55,17 +55,50 @@ class TimeLogger { 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; +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); +} + +struct DeckLinkVersion { + int major = 0; + int minor = 0; + int patch = 0; +}; + +bool parse_decklink_version(const std::string &version, DeckLinkVersion &parsed) { + if (version.empty()) { + return false; + } + + std::stringstream ss(version); + char dot1 = 0; + char dot2 = 0; + if (!(ss >> parsed.major >> dot1 >> parsed.minor >> dot2 >> parsed.patch)) { + return false; + } + + return dot1 == '.' && dot2 == '.'; +} + +bool is_decklink_version_older_than( + const DeckLinkVersion &lhs, const DeckLinkVersion &rhs) { + if (lhs.major != rhs.major) { + return lhs.major < rhs.major; + } + if (lhs.minor != rhs.minor) { + return lhs.minor < rhs.minor; + } + return lhs.patch < rhs.patch; +} +} // namespace + +void DecklinkOutput::detect_runtime_info() { + std::vector details; + api_version_.clear(); #ifdef __linux__ Dl_info dl_info; @@ -84,12 +117,13 @@ void DecklinkOutput::detect_runtime_info() { } #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 (auto *api_info = CreateDeckLinkAPIInformationInstance()) { + const char *api_version = nullptr; + if (api_info->GetString(BMDDeckLinkAPIVersion, &api_version) == S_OK && api_version) { + api_version_ = api_version; + details.emplace_back(fmt::format("api_version={}", api_version)); + } + api_info->Release(); } if (details.empty()) { @@ -345,8 +379,9 @@ void DecklinkOutput::make_intermediate_frame() { bool DecklinkOutput::init_decklink() { bool bSuccess = false; - IDeckLinkIterator *decklink_iterator = NULL; + IDeckLinkIterator *decklink_iterator = NULL; last_error_.clear(); + api_version_.clear(); output_interface_info_.clear(); try { @@ -382,24 +417,35 @@ bool DecklinkOutput::init_decklink() { if (decklink_interface_->QueryInterface( IID_IDeckLinkOutput, (void **)&decklink_output_interface_) != S_OK) { -#ifdef __linux__ +#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) { output_interface_info_ = "IID_IDeckLinkOutput_v14_2_1"; - legacy_output_interface->Release(); - const auto upgrade_message = with_runtime_details( - "Unsupported legacy Blackmagic DeckLink Linux runtime detected " - "(output_interface=IID_IDeckLinkOutput_v14_2_1). Upgrade Blackmagic " - "Desktop Video drivers to a newer release to use Blackmagic cards in " - "xStudio.", - runtime_info_); - spdlog::error("{}", upgrade_message); - spdlog::error( - "Upgrade Blackmagic Desktop Video drivers to enable Blackmagic card " - "support on Linux."); - throw std::runtime_error(upgrade_message); + DeckLinkVersion parsed_version; + const DeckLinkVersion minimum_supported{14, 2, 1}; + const bool parsed = parse_decklink_version(api_version_, parsed_version); + if (!parsed || is_decklink_version_older_than(parsed_version, minimum_supported)) { + legacy_output_interface->Release(); + const auto upgrade_message = with_runtime_details( + "Unsupported Blackmagic DeckLink Linux runtime detected. Drivers " + "older than 14.2.1 are not supported. Upgrade Blackmagic Desktop " + "Video drivers to use Blackmagic cards in xStudio.", + runtime_info_); + spdlog::error("{}", upgrade_message); + spdlog::error( + "Upgrade Blackmagic Desktop Video drivers to version 14.2.1 or " + "newer to enable Blackmagic card support on Linux."); + throw std::runtime_error(upgrade_message); + } + + decklink_output_interface_ = + reinterpret_cast(legacy_output_interface); + spdlog::warn( + "DeckLink output is using the Linux v14.2.1 compatibility ABI. " + "Upgrade Blackmagic Desktop Video drivers to 15.x or newer to use the " + "modern DeckLink binaries."); } else { throw std::runtime_error(with_runtime_details( "DeckLink runtime ABI mismatch: failed to query the video output " 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 58128cd7c..1f44f448d 100644 --- a/src/plugin/video_output/bmd_decklink/src/decklink_output.hpp +++ b/src/plugin/video_output/bmd_decklink/src/decklink_output.hpp @@ -209,9 +209,10 @@ namespace bm_decklink_plugin_1_0 { std::mutex hdr_metadata_mutex_; bool is_available_ = {false}; std::string last_error_ = {}; + std::string api_version_ = {}; std::string runtime_info_ = {}; std::string output_interface_info_ = {}; - }; + }; class AVOutputCallback : public IDeckLinkVideoOutputCallback, public IDeckLinkAudioOutputCallback { From 36472d2795b22e9af4a219a5e06ee615fad2987c Mon Sep 17 00:00:00 2001 From: "florence.beliveau" Date: Thu, 2 Apr 2026 19:35:02 +0200 Subject: [PATCH 08/12] Log DeckLink legacy compatibility HRESULTs --- .../bmd_decklink/src/decklink_output.cpp | 115 ++++++++++++++---- 1 file changed, 88 insertions(+), 27 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 678b35965..1fb2b4e70 100644 --- a/src/plugin/video_output/bmd_decklink/src/decklink_output.cpp +++ b/src/plugin/video_output/bmd_decklink/src/decklink_output.cpp @@ -63,6 +63,10 @@ std::string with_runtime_details( return fmt::format("{} ({})", message, runtime_info); } +std::string format_hresult(const HRESULT result) { + return fmt::format("0x{:08X}", static_cast(result)); +} + struct DeckLinkVersion { int major = 0; int minor = 0; @@ -410,19 +414,30 @@ bool DecklinkOutput::init_decklink() { "Blackmagic DeckLink drivers to use the features of this plugin."); } - if (decklink_iterator->Next(&decklink_interface_) != S_OK) { - 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) { + const auto next_result = decklink_iterator->Next(&decklink_interface_); + if (next_result != S_OK) { + spdlog::warn( + "DeckLink iterator returned {} while probing for a device.", + format_hresult(next_result)); + throw std::runtime_error(with_runtime_details( + "DeckLink drivers found but no device is installed", runtime_info_)); + } + + const auto modern_output_result = decklink_interface_->QueryInterface( + IID_IDeckLinkOutput, (void **)&decklink_output_interface_); + if (modern_output_result != S_OK) { + spdlog::warn( + "DeckLink modern output interface query failed with {}.", + format_hresult(modern_output_result)); #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) { + const auto legacy_output_result = decklink_interface_->QueryInterface( + IID_IDeckLinkOutput_v14_2_1, (void **)&legacy_output_interface); + if (legacy_output_result == S_OK && legacy_output_interface != nullptr) { output_interface_info_ = "IID_IDeckLinkOutput_v14_2_1"; + spdlog::warn( + "DeckLink legacy v14.2.1 output interface query succeeded with {}.", + format_hresult(legacy_output_result)); DeckLinkVersion parsed_version; const DeckLinkVersion minimum_supported{14, 2, 1}; const bool parsed = parse_decklink_version(api_version_, parsed_version); @@ -447,9 +462,12 @@ bool DecklinkOutput::init_decklink() { "Upgrade Blackmagic Desktop Video drivers to 15.x or newer to use the " "modern DeckLink binaries."); } else { + spdlog::error( + "DeckLink legacy v14.2.1 output interface query failed with {}.", + format_hresult(legacy_output_result)); throw std::runtime_error(with_runtime_details( "DeckLink runtime ABI mismatch: failed to query the video output " - "interface", + "interface", runtime_info_)); } #else @@ -457,23 +475,46 @@ bool DecklinkOutput::init_decklink() { "DeckLink runtime ABI mismatch: failed to query the video output interface", runtime_info_)); #endif - } else { - output_interface_info_ = "IID_IDeckLinkOutput"; - } + } else { + output_interface_info_ = "IID_IDeckLinkOutput"; + spdlog::info( + "DeckLink modern output interface query succeeded with {}.", + format_hresult(modern_output_result)); + } output_callback_ = new AVOutputCallback(this); if (output_callback_ == NULL) throw std::runtime_error("Failed to create Video Output Callback."); - if (decklink_output_interface_->SetScheduledFrameCompletionCallback( - static_cast(output_callback_)) != S_OK) { + spdlog::info( + "Registering DeckLink video callback via {}.", + output_interface_info_.empty() ? "unknown interface" : output_interface_info_); + const auto video_callback_result = decklink_output_interface_->SetScheduledFrameCompletionCallback( + static_cast(output_callback_)); + if (video_callback_result != S_OK) { + spdlog::error( + "DeckLink SetScheduledFrameCompletionCallback failed with {}.", + format_hresult(video_callback_result)); throw std::runtime_error("SetScheduledFrameCompletionCallback failed."); } + spdlog::info( + "DeckLink SetScheduledFrameCompletionCallback succeeded with {}.", + format_hresult(video_callback_result)); - if (decklink_output_interface_->SetAudioCallback( - static_cast(output_callback_)) != S_OK) { + spdlog::info( + "Registering DeckLink audio callback via {}.", + output_interface_info_.empty() ? "unknown interface" : output_interface_info_); + const auto audio_callback_result = decklink_output_interface_->SetAudioCallback( + static_cast(output_callback_)); + if (audio_callback_result != S_OK) { + spdlog::error( + "DeckLink SetAudioCallback failed with {}.", + format_hresult(audio_callback_result)); throw std::runtime_error("SetAudioCallback failed."); } + spdlog::info( + "DeckLink SetAudioCallback succeeded with {}.", + format_hresult(audio_callback_result)); #ifdef _WIN32 // Create an IDeckLinkVideoConversion interface object to provide pixel format @@ -652,11 +693,17 @@ bool DecklinkOutput::start_sdi_output() { uiFPS = ((frame_timescale_ + (frame_duration_ - 1)) / frame_duration_); - if (decklink_output_interface_->EnableVideoOutput( - display_mode->GetDisplayMode(), bmdVideoOutputFlagDefault) != - S_OK) { + const auto enable_video_result = decklink_output_interface_->EnableVideoOutput( + display_mode->GetDisplayMode(), bmdVideoOutputFlagDefault); + if (enable_video_result != S_OK) { + spdlog::error( + "DeckLink EnableVideoOutput failed with {}.", + format_hresult(enable_video_result)); throw std::runtime_error("EnableVideoOutput call failed."); } + spdlog::info( + "DeckLink EnableVideoOutput succeeded with {}.", + format_hresult(enable_video_result)); video_output_enabled_ = true; } } @@ -669,13 +716,20 @@ bool DecklinkOutput::start_sdi_output() { uiTotalFrames = 0; // Set the audio output mode - if (decklink_output_interface_->EnableAudioOutput( - bmdAudioSampleRate48kHz, - bmdAudioSampleType16bitInteger, - 2, // num channels - bmdAudioOutputStreamTimestamped) != S_OK) { + const auto enable_audio_result = decklink_output_interface_->EnableAudioOutput( + bmdAudioSampleRate48kHz, + bmdAudioSampleType16bitInteger, + 2, // num channels + bmdAudioOutputStreamTimestamped); + if (enable_audio_result != S_OK) { + spdlog::error( + "DeckLink EnableAudioOutput failed with {}.", + format_hresult(enable_audio_result)); throw std::runtime_error("Failed to enable audio output."); } + spdlog::info( + "DeckLink EnableAudioOutput succeeded with {}.", + format_hresult(enable_audio_result)); audio_output_enabled_ = true; @@ -683,9 +737,16 @@ bool DecklinkOutput::start_sdi_output() { samples_delivered_ = 0; - if (decklink_output_interface_->BeginAudioPreroll() != S_OK) { + const auto preroll_result = decklink_output_interface_->BeginAudioPreroll(); + if (preroll_result != S_OK) { + spdlog::error( + "DeckLink BeginAudioPreroll failed with {}.", + format_hresult(preroll_result)); throw std::runtime_error("Failed to pre-roll audio output."); } + spdlog::info( + "DeckLink BeginAudioPreroll succeeded with {}.", + format_hresult(preroll_result)); decklink_output_interface_->StartScheduledPlayback(0, 100, 1.0); scheduled_playback_started_ = true; From e5362f97806471eda700a5cb9504cfc57a631e73 Mon Sep 17 00:00:00 2001 From: "florence.beliveau" Date: Fri, 3 Apr 2026 13:22:23 +0200 Subject: [PATCH 09/12] Split DeckLink legacy audio and video callbacks --- .../bmd_decklink/src/decklink_output.cpp | 2097 +++++++++-------- .../bmd_decklink/src/decklink_output.hpp | 474 ++-- 2 files changed, 1324 insertions(+), 1247 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 1fb2b4e70..932deb021 100644 --- a/src/plugin/video_output/bmd_decklink/src/decklink_output.cpp +++ b/src/plugin/video_output/bmd_decklink/src/decklink_output.cpp @@ -1,60 +1,60 @@ -// SPDX-License-Identifier: Apache-2.0 -#include "decklink_output.hpp" -#include "decklink_plugin.hpp" +// SPDX-License-Identifier: Apache-2.0 +#include "decklink_output.hpp" +#include "decklink_plugin.hpp" #include "xstudio/utility/logging.hpp" #include "xstudio/utility/chrono.hpp" #include "xstudio/enums.hpp" #include #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; - -// Uncomment to use this debug timer to see how the frame conversion is performing -namespace { - -class LogBot { - public: - std::map> frame_times_; - void log(const std::string l, int64_t t) { - frame_times_[l].push_back(t); - if ((frame_times_[l].size() % 24) == 0) { - int64_t total = 0; - for (auto v : frame_times_[l]) - total += v; - std::cerr << "Average time for " << l << ": " - << double(total / frame_times_[l].size()) / 1000.0 << "ms\n"; - frame_times_[l].clear(); - } - } -}; -static LogBot s_logBot; - -class TimeLogger { - public: - TimeLogger(const std::string &label) - : label_(label), start_time_(std::chrono::high_resolution_clock::now()) {} - ~TimeLogger() { - s_logBot.log( - label_, - std::chrono::duration_cast( - std::chrono::high_resolution_clock::now() - start_time_) - .count()); - } - std::string label_; - std::chrono::high_resolution_clock::time_point start_time_; -}; - +#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; + +// Uncomment to use this debug timer to see how the frame conversion is performing +namespace { + +class LogBot { + public: + std::map> frame_times_; + void log(const std::string l, int64_t t) { + frame_times_[l].push_back(t); + if ((frame_times_[l].size() % 24) == 0) { + int64_t total = 0; + for (auto v : frame_times_[l]) + total += v; + std::cerr << "Average time for " << l << ": " + << double(total / frame_times_[l].size()) / 1000.0 << "ms\n"; + frame_times_[l].clear(); + } + } +}; +static LogBot s_logBot; + +class TimeLogger { + public: + TimeLogger(const std::string &label) + : label_(label), start_time_(std::chrono::high_resolution_clock::now()) {} + ~TimeLogger() { + s_logBot.log( + label_, + std::chrono::duration_cast( + std::chrono::high_resolution_clock::now() - start_time_) + .count()); + } + 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()) { @@ -103,24 +103,24 @@ bool is_decklink_version_older_than( void DecklinkOutput::detect_runtime_info() { std::vector details; api_version_.clear(); - -#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 - + +#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) { @@ -128,118 +128,118 @@ void DecklinkOutput::detect_runtime_info() { 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 - // on the system abort construction of the plugin (caught by plugin manager) - // The reason we do this is that we don't want the plugin to be available at - // all in the UI if the drivers aren't present, as it would just lead to - // user confusion and support requests about why the plugin doesn't work. -#ifdef __APPLE__ - CFURLRef bundleURL = CFURLCreateWithFileSystemPath( - kCFAllocatorDefault, - CFSTR("/Library/Frameworks/DeckLinkAPI.framework"), - kCFURLPOSIXPathStyle, - true); - bool drivers_found = false; - if (bundleURL) { - CFBundleRef bundle = CFBundleCreate(kCFAllocatorDefault, bundleURL); - drivers_found = (bundle != NULL); - if (bundle) - CFRelease(bundle); - CFRelease(bundleURL); - } - if (!drivers_found) { - throw std::runtime_error("drivers not found."); - return; - } -#elif defined(__linux__) - if (!dlopen(kDeckLinkAPI_Name, RTLD_NOW | RTLD_GLOBAL)) { - throw std::runtime_error("drivers not found."); - return; - } -#else // Windows - IDeckLinkIterator *pDLIterator = NULL; - HRESULT result; - result = CoCreateInstance( - CLSID_CDeckLinkIterator, - NULL, - CLSCTX_ALL, - IID_IDeckLinkIterator, - (void **)&pDLIterator); - if (FAILED(result)) { - throw std::runtime_error("drivers not found."); - } - - // If no device found, that will be caught later. - /*IDeckLink* deckLink = nullptr; - if (pDLIterator->Next(&deckLink) != S_OK) - { - if (deckLink != NULL) - { - deckLink->Release(); - } else { - throw std::runtime_error("no DeckLink devices found."); - } - }*/ -#endif -} - -DecklinkOutput::DecklinkOutput(BMDecklinkPlugin *decklink_xstudio_plugin) - : pFrameBuf(NULL), - decklink_interface_(NULL), - decklink_output_interface_(NULL), - decklink_xstudio_plugin_(decklink_xstudio_plugin) { - - is_available_ = init_decklink(); -} - -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"); -} - + } + + 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 + // on the system abort construction of the plugin (caught by plugin manager) + // The reason we do this is that we don't want the plugin to be available at + // all in the UI if the drivers aren't present, as it would just lead to + // user confusion and support requests about why the plugin doesn't work. +#ifdef __APPLE__ + CFURLRef bundleURL = CFURLCreateWithFileSystemPath( + kCFAllocatorDefault, + CFSTR("/Library/Frameworks/DeckLinkAPI.framework"), + kCFURLPOSIXPathStyle, + true); + bool drivers_found = false; + if (bundleURL) { + CFBundleRef bundle = CFBundleCreate(kCFAllocatorDefault, bundleURL); + drivers_found = (bundle != NULL); + if (bundle) + CFRelease(bundle); + CFRelease(bundleURL); + } + if (!drivers_found) { + throw std::runtime_error("drivers not found."); + return; + } +#elif defined(__linux__) + if (!dlopen(kDeckLinkAPI_Name, RTLD_NOW | RTLD_GLOBAL)) { + throw std::runtime_error("drivers not found."); + return; + } +#else // Windows + IDeckLinkIterator *pDLIterator = NULL; + HRESULT result; + result = CoCreateInstance( + CLSID_CDeckLinkIterator, + NULL, + CLSCTX_ALL, + IID_IDeckLinkIterator, + (void **)&pDLIterator); + if (FAILED(result)) { + throw std::runtime_error("drivers not found."); + } + + // If no device found, that will be caught later. + /*IDeckLink* deckLink = nullptr; + if (pDLIterator->Next(&deckLink) != S_OK) + { + if (deckLink != NULL) + { + deckLink->Release(); + } else { + throw std::runtime_error("no DeckLink devices found."); + } + }*/ +#endif +} + +DecklinkOutput::DecklinkOutput(BMDecklinkPlugin *decklink_xstudio_plugin) + : pFrameBuf(NULL), + decklink_interface_(NULL), + decklink_output_interface_(NULL), + decklink_xstudio_plugin_(decklink_xstudio_plugin) { + + is_available_ = init_decklink(); +} + +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) { @@ -262,20 +262,24 @@ void DecklinkOutput::release_resources() { if (decklink_interface_ != NULL) { decklink_interface_->Release(); } - if (output_callback_ != NULL) { - output_callback_->Release(); - } - - if (frame_converter_ != NULL) { - frame_converter_->Release(); - } - - if (intermediate_frame_ != nullptr) { - intermediate_frame_->Release(); - intermediate_frame_ = nullptr; - } - - output_callback_ = nullptr; + if (video_output_callback_ != NULL) { + video_output_callback_->Release(); + } + if (audio_output_callback_ != NULL) { + audio_output_callback_->Release(); + } + + if (frame_converter_ != NULL) { + frame_converter_->Release(); + } + + if (intermediate_frame_ != nullptr) { + intermediate_frame_->Release(); + intermediate_frame_ = nullptr; + } + + video_output_callback_ = nullptr; + audio_output_callback_ = nullptr; frame_converter_ = nullptr; decklink_output_interface_ = nullptr; decklink_interface_ = nullptr; @@ -283,137 +287,137 @@ void DecklinkOutput::release_resources() { video_output_enabled_ = false; audio_output_enabled_ = false; } - -void DecklinkOutput::set_preroll() { - IDeckLinkMutableVideoFrame *decklink_video_frame = NULL; - - // Set 3 frame preroll - try { - - for (uint32_t i = 0; i < 3; i++) { - - int32_t rowBytes; - if (decklink_output_interface_->RowBytesForPixelFormat( - current_pix_format_, frame_width_, &rowBytes) != S_OK) { - throw std::runtime_error("Failed on call to RowBytesForPixelFormat."); - } - - // Flip frame vertical, because OpenGL rendering starts from left bottom corner - if (decklink_output_interface_->CreateVideoFrame( - frame_width_, - frame_height_, - rowBytes, - current_pix_format_, - bmdFrameFlagFlipVertical, - &decklink_video_frame) != S_OK) - throw std::runtime_error("Failed on CreateVideoFrame"); - - update_frame_metadata(decklink_video_frame); - - if (decklink_output_interface_->ScheduleVideoFrame( - decklink_video_frame, - (uiTotalFrames * frame_duration_), - frame_duration_, - frame_timescale_) != S_OK) - throw std::runtime_error("Failed on ScheduleVideoFrame"); - - /* The local reference to the IDeckLinkVideoFrame is released here, as the ownership - * has now been passed to the DeckLinkAPI via ScheduleVideoFrame. - * - * After the API has finished with the frame, it is returned to the application via - * ScheduledFrameCompleted. In ScheduledFrameCompleted, this application updates the - * video frame and passes it to ScheduleVideoFrame, returning ownership to the - * DeckLink API. - */ - decklink_video_frame->Release(); - decklink_video_frame = NULL; - - uiTotalFrames++; - } - } catch (std::exception &e) { - - if (decklink_video_frame) { - decklink_video_frame->Release(); - decklink_video_frame = NULL; - } - report_error(e.what()); - } -} - -void DecklinkOutput::make_intermediate_frame() { - - // We need to support some othe pixel formats less likely to be needed but - // nevertheless possibly required. For this we convert to BMD 12 bit RGB - // (which seems to be the most efficient conversion on our side) and then - // use Decklink API to convert from that to the desired output format. - int32_t referenceBytesPerRow; - - HRESULT result = decklink_output_interface_->RowBytesForPixelFormat( - bmdFormat12BitRGBLE, frame_width_, &referenceBytesPerRow); - - if (result != S_OK) { - throw std::runtime_error("Failed to get row bytes for reference video frame"); - } - - if (intermediate_frame_) { - if (intermediate_frame_->GetWidth() != frame_width_ || - intermediate_frame_->GetHeight() != frame_height_) { - intermediate_frame_->Release(); - intermediate_frame_ = nullptr; - } - } - - if (!intermediate_frame_) { - // Create a black frame in 8 bit YUV and convert to desired format - result = decklink_output_interface_->CreateVideoFrame( - frame_width_, - frame_height_, - referenceBytesPerRow, - bmdFormat12BitRGBLE, - bmdFrameFlagDefault, - &intermediate_frame_); - - if (result != S_OK) { - throw std::runtime_error("Failed to create reference video frame"); - } - } -} - - -bool DecklinkOutput::init_decklink() { - bool bSuccess = false; - + +void DecklinkOutput::set_preroll() { + IDeckLinkMutableVideoFrame *decklink_video_frame = NULL; + + // Set 3 frame preroll + try { + + for (uint32_t i = 0; i < 3; i++) { + + int32_t rowBytes; + if (decklink_output_interface_->RowBytesForPixelFormat( + current_pix_format_, frame_width_, &rowBytes) != S_OK) { + throw std::runtime_error("Failed on call to RowBytesForPixelFormat."); + } + + // Flip frame vertical, because OpenGL rendering starts from left bottom corner + if (decklink_output_interface_->CreateVideoFrame( + frame_width_, + frame_height_, + rowBytes, + current_pix_format_, + bmdFrameFlagFlipVertical, + &decklink_video_frame) != S_OK) + throw std::runtime_error("Failed on CreateVideoFrame"); + + update_frame_metadata(decklink_video_frame); + + if (decklink_output_interface_->ScheduleVideoFrame( + decklink_video_frame, + (uiTotalFrames * frame_duration_), + frame_duration_, + frame_timescale_) != S_OK) + throw std::runtime_error("Failed on ScheduleVideoFrame"); + + /* The local reference to the IDeckLinkVideoFrame is released here, as the ownership + * has now been passed to the DeckLinkAPI via ScheduleVideoFrame. + * + * After the API has finished with the frame, it is returned to the application via + * ScheduledFrameCompleted. In ScheduledFrameCompleted, this application updates the + * video frame and passes it to ScheduleVideoFrame, returning ownership to the + * DeckLink API. + */ + decklink_video_frame->Release(); + decklink_video_frame = NULL; + + uiTotalFrames++; + } + } catch (std::exception &e) { + + if (decklink_video_frame) { + decklink_video_frame->Release(); + decklink_video_frame = NULL; + } + report_error(e.what()); + } +} + +void DecklinkOutput::make_intermediate_frame() { + + // We need to support some othe pixel formats less likely to be needed but + // nevertheless possibly required. For this we convert to BMD 12 bit RGB + // (which seems to be the most efficient conversion on our side) and then + // use Decklink API to convert from that to the desired output format. + int32_t referenceBytesPerRow; + + HRESULT result = decklink_output_interface_->RowBytesForPixelFormat( + bmdFormat12BitRGBLE, frame_width_, &referenceBytesPerRow); + + if (result != S_OK) { + throw std::runtime_error("Failed to get row bytes for reference video frame"); + } + + if (intermediate_frame_) { + if (intermediate_frame_->GetWidth() != frame_width_ || + intermediate_frame_->GetHeight() != frame_height_) { + intermediate_frame_->Release(); + intermediate_frame_ = nullptr; + } + } + + if (!intermediate_frame_) { + // Create a black frame in 8 bit YUV and convert to desired format + result = decklink_output_interface_->CreateVideoFrame( + frame_width_, + frame_height_, + referenceBytesPerRow, + bmdFormat12BitRGBLE, + bmdFrameFlagDefault, + &intermediate_frame_); + + if (result != S_OK) { + throw std::runtime_error("Failed to create reference video frame"); + } + } +} + + +bool DecklinkOutput::init_decklink() { + bool bSuccess = false; + IDeckLinkIterator *decklink_iterator = NULL; last_error_.clear(); api_version_.clear(); output_interface_info_.clear(); - - try { - detect_runtime_info(); - -#ifdef _WIN32 - HRESULT result; - result = CoCreateInstance( - CLSID_CDeckLinkIterator, - NULL, - CLSCTX_ALL, - IID_IDeckLinkIterator, - (void **)&decklink_iterator); - if (FAILED(result)) { - throw std::runtime_error( - "Please install the Blackmagic DeckLink drivers to use the features of this " - "application.This application requires the DeckLink drivers installed."); - return false; - } -#else - decklink_iterator = CreateDeckLinkIteratorInstance(); -#endif - if (decklink_iterator == NULL) { - throw std::runtime_error( - "This plugin requires the DeckLink drivers installed. Please install the " - "Blackmagic DeckLink drivers to use the features of this plugin."); - } - + + try { + detect_runtime_info(); + +#ifdef _WIN32 + HRESULT result; + result = CoCreateInstance( + CLSID_CDeckLinkIterator, + NULL, + CLSCTX_ALL, + IID_IDeckLinkIterator, + (void **)&decklink_iterator); + if (FAILED(result)) { + throw std::runtime_error( + "Please install the Blackmagic DeckLink drivers to use the features of this " + "application.This application requires the DeckLink drivers installed."); + return false; + } +#else + decklink_iterator = CreateDeckLinkIteratorInstance(); +#endif + if (decklink_iterator == NULL) { + throw std::runtime_error( + "This plugin requires the DeckLink drivers installed. Please install the " + "Blackmagic DeckLink drivers to use the features of this plugin."); + } + const auto next_result = decklink_iterator->Next(&decklink_interface_); if (next_result != S_OK) { spdlog::warn( @@ -468,29 +472,33 @@ bool DecklinkOutput::init_decklink() { 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 + 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"; spdlog::info( "DeckLink modern output interface query succeeded with {}.", format_hresult(modern_output_result)); } - - output_callback_ = new AVOutputCallback(this); - if (output_callback_ == NULL) - throw std::runtime_error("Failed to create Video Output Callback."); - + + video_output_callback_ = new AVOutputCallback(this); + if (video_output_callback_ == NULL) + throw std::runtime_error("Failed to create Video Output Callback."); + + audio_output_callback_ = new AudioOutputCallback(this); + if (audio_output_callback_ == NULL) + throw std::runtime_error("Failed to create Audio Output Callback."); + spdlog::info( "Registering DeckLink video callback via {}.", output_interface_info_.empty() ? "unknown interface" : output_interface_info_); const auto video_callback_result = decklink_output_interface_->SetScheduledFrameCompletionCallback( - static_cast(output_callback_)); + static_cast(video_output_callback_)); if (video_callback_result != S_OK) { spdlog::error( "DeckLink SetScheduledFrameCompletionCallback failed with {}.", @@ -505,7 +513,7 @@ bool DecklinkOutput::init_decklink() { "Registering DeckLink audio callback via {}.", output_interface_info_.empty() ? "unknown interface" : output_interface_info_); const auto audio_callback_result = decklink_output_interface_->SetAudioCallback( - static_cast(output_callback_)); + static_cast(audio_output_callback_)); if (audio_callback_result != S_OK) { spdlog::error( "DeckLink SetAudioCallback failed with {}.", @@ -515,184 +523,188 @@ bool DecklinkOutput::init_decklink() { spdlog::info( "DeckLink SetAudioCallback succeeded with {}.", format_hresult(audio_callback_result)); - -#ifdef _WIN32 - // Create an IDeckLinkVideoConversion interface object to provide pixel format - // conversion of video frame. - result = CoCreateInstance( - CLSID_CDeckLinkVideoConversion, - NULL, - CLSCTX_ALL, - IID_IDeckLinkVideoConversion, - (void **)&frame_converter_); - if (FAILED(result)) { - throw std::runtime_error( - "A DeckLink Video Conversion interface could not be created."); - } - -#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 - + +#ifdef _WIN32 + // Create an IDeckLinkVideoConversion interface object to provide pixel format + // conversion of video frame. + result = CoCreateInstance( + CLSID_CDeckLinkVideoConversion, + NULL, + CLSCTX_ALL, + IID_IDeckLinkVideoConversion, + (void **)&frame_converter_); + if (FAILED(result)) { + throw std::runtime_error( + "A DeckLink Video Conversion interface could not be created."); + } + +#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; is_available_ = true; log_runtime_info(); spdlog::info("DeckLink runtime is supported."); 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()); - release_resources(); - } - - if (decklink_iterator != NULL) { - decklink_iterator->Release(); - decklink_iterator = NULL; - } - - return bSuccess; -} - -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 - if (decklink_output_interface_->GetDisplayModeIterator(&display_mode_iterator) == - S_OK) { - - while (display_mode_iterator->Next(&display_mode) == S_OK) { - - DECKLINK_STR modeName = nullptr; - display_mode->GetName(&modeName); - std::string buf = decklink_string_to_std(modeName); - decklink_free_string(modeName); - display_mode->GetFrameRate(&frame_duration_, &frame_timescale_); - - // only names with 'i' in are interalaced as far as I can tell - const bool interlaced = buf.find("i") != std::string::npos; - - // I've decided that support for interlaced modes is not useful! - if (interlaced) - continue; - - const std::string resolution_string = - fmt::format("{} x {}", display_mode->GetWidth(), display_mode->GetHeight()); - std::string refresh_rate = - fmt::format("{:.3f}", double(frame_timescale_) / double(frame_duration_)); - // erase all but the last trailing zero - while (refresh_rate.back() == '0' && - refresh_rate.rfind(".0") != (refresh_rate.size() - 2)) { - refresh_rate.pop_back(); - } - - refresh_rate_per_output_resolution_[resolution_string].push_back(refresh_rate); - display_modes_[std::make_pair(resolution_string, refresh_rate)] = - display_mode->GetDisplayMode(); - } - } - } catch (std::exception &e) { - - report_error(e.what()); - } - - if (display_mode) { - display_mode->Release(); - display_mode = NULL; - } - - if (display_mode_iterator) { - display_mode_iterator->Release(); - display_mode_iterator = NULL; - } -} - -std::vector -DecklinkOutput::get_available_refresh_rates(const std::string &output_resolution) const { - auto p = refresh_rate_per_output_resolution_.find(output_resolution); - if (p != refresh_rate_per_output_resolution_.end()) { - return p->second; - } - return std::vector({"Bad Resolution"}); -} - -void DecklinkOutput::set_display_mode( - const std::string &resolution, - const std::string &refresh_rate, - const BMDPixelFormat pix_format) { - - auto p = display_modes_.find(std::make_pair(resolution, refresh_rate)); - if (p == display_modes_.end()) { - throw std::runtime_error( - fmt::format("Failed to find a display mode for {} @ {}", resolution, refresh_rate)); - } - current_pix_format_ = pix_format; - current_display_mode_ = p->second; -} - - -bool DecklinkOutput::start_sdi_output() { - - bool bSuccess = false; - - IDeckLinkDisplayModeIterator *display_mode_iterator = NULL; - IDeckLinkDisplayMode *display_mode = NULL; - - try { - + + } 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_) { + decklink_output_interface_->SetScheduledFrameCompletionCallback(nullptr); + decklink_output_interface_->SetAudioCallback(nullptr); + } + release_resources(); + } + + if (decklink_iterator != NULL) { + decklink_iterator->Release(); + decklink_iterator = NULL; + } + + return bSuccess; +} + +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 + if (decklink_output_interface_->GetDisplayModeIterator(&display_mode_iterator) == + S_OK) { + + while (display_mode_iterator->Next(&display_mode) == S_OK) { + + DECKLINK_STR modeName = nullptr; + display_mode->GetName(&modeName); + std::string buf = decklink_string_to_std(modeName); + decklink_free_string(modeName); + display_mode->GetFrameRate(&frame_duration_, &frame_timescale_); + + // only names with 'i' in are interalaced as far as I can tell + const bool interlaced = buf.find("i") != std::string::npos; + + // I've decided that support for interlaced modes is not useful! + if (interlaced) + continue; + + const std::string resolution_string = + fmt::format("{} x {}", display_mode->GetWidth(), display_mode->GetHeight()); + std::string refresh_rate = + fmt::format("{:.3f}", double(frame_timescale_) / double(frame_duration_)); + // erase all but the last trailing zero + while (refresh_rate.back() == '0' && + refresh_rate.rfind(".0") != (refresh_rate.size() - 2)) { + refresh_rate.pop_back(); + } + + refresh_rate_per_output_resolution_[resolution_string].push_back(refresh_rate); + display_modes_[std::make_pair(resolution_string, refresh_rate)] = + display_mode->GetDisplayMode(); + } + } + } catch (std::exception &e) { + + report_error(e.what()); + } + + if (display_mode) { + display_mode->Release(); + display_mode = NULL; + } + + if (display_mode_iterator) { + display_mode_iterator->Release(); + display_mode_iterator = NULL; + } +} + +std::vector +DecklinkOutput::get_available_refresh_rates(const std::string &output_resolution) const { + auto p = refresh_rate_per_output_resolution_.find(output_resolution); + if (p != refresh_rate_per_output_resolution_.end()) { + return p->second; + } + return std::vector({"Bad Resolution"}); +} + +void DecklinkOutput::set_display_mode( + const std::string &resolution, + const std::string &refresh_rate, + const BMDPixelFormat pix_format) { + + auto p = display_modes_.find(std::make_pair(resolution, refresh_rate)); + if (p == display_modes_.end()) { + throw std::runtime_error( + fmt::format("Failed to find a display mode for {} @ {}", resolution, refresh_rate)); + } + current_pix_format_ = pix_format; + current_display_mode_ = p->second; +} + + +bool DecklinkOutput::start_sdi_output() { + + bool bSuccess = false; + + IDeckLinkDisplayModeIterator *display_mode_iterator = NULL; + IDeckLinkDisplayMode *display_mode = NULL; + + try { + if (!decklink_output_interface_) { throw std::runtime_error( last_error_.empty() ? "No DeckLink device is available." : last_error_); } - - bool mode_matched = false; - // Get first avaliable video mode for Output - if (decklink_output_interface_->GetDisplayModeIterator(&display_mode_iterator) == - S_OK) { - while (display_mode_iterator->Next(&display_mode) == S_OK) { - if (display_mode->GetDisplayMode() == current_display_mode_) { - - mode_matched = true; - { - // get the name of the display mode, for display only - DECKLINK_STR modeName = nullptr; - display_mode->GetName(&modeName); - display_mode_name_ = decklink_string_to_std(modeName); - decklink_free_string(modeName); - } - - report_status( - fmt::format( - "Starting Decklink output loop in mode {}.", display_mode_name_), - false); - - frame_width_ = display_mode->GetWidth(); - frame_height_ = display_mode->GetHeight(); - display_mode->GetFrameRate(&frame_duration_, &frame_timescale_); - - uiFPS = ((frame_timescale_ + (frame_duration_ - 1)) / frame_duration_); - + + bool mode_matched = false; + // Get first avaliable video mode for Output + if (decklink_output_interface_->GetDisplayModeIterator(&display_mode_iterator) == + S_OK) { + while (display_mode_iterator->Next(&display_mode) == S_OK) { + if (display_mode->GetDisplayMode() == current_display_mode_) { + + mode_matched = true; + { + // get the name of the display mode, for display only + DECKLINK_STR modeName = nullptr; + display_mode->GetName(&modeName); + display_mode_name_ = decklink_string_to_std(modeName); + decklink_free_string(modeName); + } + + report_status( + fmt::format( + "Starting Decklink output loop in mode {}.", display_mode_name_), + false); + + frame_width_ = display_mode->GetWidth(); + frame_height_ = display_mode->GetHeight(); + display_mode->GetFrameRate(&frame_duration_, &frame_timescale_); + + uiFPS = ((frame_timescale_ + (frame_duration_ - 1)) / frame_duration_); + const auto enable_video_result = decklink_output_interface_->EnableVideoOutput( display_mode->GetDisplayMode(), bmdVideoOutputFlagDefault); if (enable_video_result != S_OK) { @@ -708,14 +720,14 @@ bool DecklinkOutput::start_sdi_output() { } } } - - if (!mode_matched) { - throw std::runtime_error("Failed to find display mode."); - } - - uiTotalFrames = 0; - - // Set the audio output mode + + if (!mode_matched) { + throw std::runtime_error("Failed to find display mode."); + } + + uiTotalFrames = 0; + + // Set the audio output mode const auto enable_audio_result = decklink_output_interface_->EnableAudioOutput( bmdAudioSampleRate48kHz, bmdAudioSampleType16bitInteger, @@ -734,9 +746,9 @@ bool DecklinkOutput::start_sdi_output() { set_preroll(); - - samples_delivered_ = 0; - + + samples_delivered_ = 0; + const auto preroll_result = decklink_output_interface_->BeginAudioPreroll(); if (preroll_result != S_OK) { spdlog::error( @@ -769,31 +781,31 @@ bool DecklinkOutput::start_sdi_output() { report_error(e.what()); } - - if (display_mode) { - display_mode->Release(); - display_mode = NULL; - } - - if (display_mode_iterator) { - display_mode_iterator->Release(); - display_mode_iterator = NULL; - } - - return bSuccess; -} - -bool DecklinkOutput::stop_sdi_output(const std::string &error_message) { - - running_ = false; - decklink_xstudio_plugin_->stop(); - - if (!error_message.empty()) { - report_error(error_message); - } else { - report_status("SDI Output Paused.", false); - } - + + if (display_mode) { + display_mode->Release(); + display_mode = NULL; + } + + if (display_mode_iterator) { + display_mode_iterator->Release(); + display_mode_iterator = NULL; + } + + return bSuccess; +} + +bool DecklinkOutput::stop_sdi_output(const std::string &error_message) { + + running_ = false; + decklink_xstudio_plugin_->stop(); + + if (!error_message.empty()) { + report_error(error_message); + } else { + report_status("SDI Output Paused.", false); + } + spdlog::info("Stopping Decklink output loop. {}", error_message); if (decklink_output_interface_) { @@ -816,398 +828,398 @@ bool DecklinkOutput::stop_sdi_output(const std::string &error_message) { fetch_more_samples_from_xstudio_ = true; } audio_samples_cv_.notify_all(); - - mutex_.lock(); - - free(pFrameBuf); - pFrameBuf = NULL; - - mutex_.unlock(); - - spdlog::info("Stopping Decklink output loop done. {}", error_message); - - return true; -} - -void DecklinkOutput::StartStop() { - if (!running_) - start_sdi_output(); - else - stop_sdi_output(); -} - -void DecklinkOutput::incoming_frame(const media_reader::ImageBufPtr &incoming) { - - // this is called from xstudio managed thread, which is independent of - // the decklink output thread control - frames_mutex_.lock(); - current_frame_ = incoming; - frames_mutex_.unlock(); -} - -namespace { -void multithreadMemCopy(void *_dst, void *_src, size_t buf_size, const int n_threads) { - - // Note: my instinct tells me that spawning threads for - // every copy operation (which might happen 60 times a second) - // is not efficient but it seems that having a threadpool doesn't - // make any real difference, the overhead of thread creation - // is tiny. - std::vector memcpy_threads; - size_t step = ((buf_size / n_threads) / 4096) * 4096; - - uint8_t *dst = (uint8_t *)_dst; - uint8_t *src = (uint8_t *)_src; - - for (int i = 0; i < n_threads; ++i) { - memcpy_threads.emplace_back(memcpy, dst, src, std::min(buf_size, step)); - dst += step; - src += step; - buf_size -= step; - } - - // ensure any threads still running to copy data to this texture are done - for (auto &t : memcpy_threads) { - if (t.joinable()) - t.join(); - } -} - -} // namespace - -#define CHECK_BIT(var, pos) ((var) & (1 << (pos))) - -void DecklinkOutput::report_status( - const std::string &status_message, const bool sdi_output_is_active) { - - utility::JsonStore j; - j["status_message"] = status_message; - j["sdi_output_is_active"] = sdi_output_is_active; - j["error_state"] = false; - decklink_xstudio_plugin_->send_status(j); -} - -void DecklinkOutput::report_error(const std::string &status_message) { - - utility::JsonStore j; - j["status_message"] = status_message; - j["sdi_output_is_active"] = false; - j["error_state"] = true; - decklink_xstudio_plugin_->send_status(j); -} - -void DecklinkOutput::fill_decklink_video_frame(IDeckLinkVideoFrame *decklink_video_frame) { - - // this function (fill_decklink_video_frame) is called by the Decklink API at a steady beat - // matching the refresh rate of the SDI output. We can therefore use it to tell our - // offscreen viewport to render a new frame ready for the subsequent call to this function. - // Remember The frame rendered by xstduio is delivered to us via the incoming_frame callback - // and, as long as xstudio is able to render the video frame in somthing less than the - // refresh period for the SDI output, it should have been delivered before we re-enter this - // function. - // - // The time value passed into this request is our best estimate of when the frame that we - // are requesting will actually be put on the screen. - decklink_xstudio_plugin_->request_video_frame(utility::clock::now()); - - - // We also need to make this crucial call to tell xstudio's offscreen viewport when the - // last video frame was put on screen. It uses the regular beat of these calls to work - // out the refresh rate of the video output and therefore do an accurate 'pulldown' when - // evaluating the playhead position for the next frame to go on screen. - // In the case of the Decklink, we know that this function (fill_decklink_video_frame) is - // being called with a beat matching the SDI refresh (as long as our code immediately below - // completes well inside/ that period) - decklink_xstudio_plugin_->video_frame_consumed(utility::clock::now()); - - static auto tp = utility::clock::now(); - auto tp1 = utility::clock::now(); - tp = tp1; - - mutex_.lock(); - - // SDK v15.3: GetBytes() moved from IDeckLinkVideoFrame to IDeckLinkVideoBuffer - IDeckLinkVideoBuffer *video_buffer = nullptr; - decklink_video_frame->QueryInterface(IID_IDeckLinkVideoBuffer, (void **)&video_buffer); - - frames_mutex_.lock(); - media_reader::ImageBufPtr the_frame = current_frame_; - frames_mutex_.unlock(); - - if (the_frame) { - - auto tp = utility::clock::now(); - - if (the_frame->size() >= decklink_video_frame->GetRowBytes() * frame_height_) { - - int xstudio_buf_pixel_format = the_frame->params().value("pixel_format", 0); - - if (xstudio_buf_pixel_format == ui::viewport::RGBA_16 && video_buffer) { - - // On macOS, DMA buffers may need StartAccess to map into CPU memory - HRESULT sa_hr = video_buffer->StartAccess(bmdBufferAccessReadAndWrite); - try { - - void *pFrame = nullptr; - HRESULT gb_hr = video_buffer->GetBytes((void **)&pFrame); - int num_pix = - decklink_video_frame->GetWidth() * decklink_video_frame->GetHeight(); - void *src_buf = the_frame->buffer(); - - // Validate pointers and ensure source buffer is large enough - // for num_pix RGBA_16 pixels (8 bytes each) - if (!pFrame || !src_buf) { - throw std::runtime_error( - fmt::format( - "Decklink: null buffer in fill_decklink_video_frame " - "(pFrame={}, src_buf={}, num_pix={}, StartAccess=0x{:x}, " - "GetBytes=0x{:x})", - pFrame, - src_buf, - num_pix, - (unsigned long)sa_hr, - (unsigned long)gb_hr)); - } else if (the_frame->size() < (size_t)num_pix * 8) { - throw std::runtime_error( - fmt::format( - "Decklink: source buffer too small " - "(size={}, need={}, frame={}x{}, src_format=RGBA_16)", - the_frame->size(), - (size_t)num_pix * 8, - decklink_video_frame->GetWidth(), - decklink_video_frame->GetHeight())); - } else if (decklink_video_frame->GetPixelFormat() == bmdFormat10BitRGB) { - - // TimeLogger l("RGBA16_to_10bitRGB"); - pixel_swizzler_.copy_frame_buffer_10bit( - pFrame, src_buf, num_pix); - - } else if (decklink_video_frame->GetPixelFormat() == bmdFormat10BitRGBXLE) { - - // TimeLogger l("RGBA16_to_10bitRGBXLE"); - pixel_swizzler_.copy_frame_buffer_10bit( - pFrame, src_buf, num_pix); - - } else if (decklink_video_frame->GetPixelFormat() == bmdFormat10BitRGBX) { - - // TimeLogger l("RGBA16_to_10bitRGBX"); - pixel_swizzler_.copy_frame_buffer_10bit( - pFrame, src_buf, num_pix); - - } else if (decklink_video_frame->GetPixelFormat() == bmdFormat12BitRGB) { - - // TimeLogger l("RGBA16_to_12bitRGB"); - pixel_swizzler_.copy_frame_buffer_12bit( - pFrame, src_buf, num_pix); - - } else if (decklink_video_frame->GetPixelFormat() == bmdFormat12BitRGBLE) { - - // TimeLogger l("RGBA16_to_12bitRGBLE"); - pixel_swizzler_.copy_frame_buffer_12bit( - 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"); - make_intermediate_frame(); - - IDeckLinkVideoBuffer *intermediate_video_buffer = nullptr; - auto r = intermediate_frame_->QueryInterface( - IID_IDeckLinkVideoBuffer, (void **)&intermediate_video_buffer); - if (r != S_OK || !intermediate_video_buffer) { - throw std::runtime_error("Failed to get conversion video buffer"); - } - - r = intermediate_video_buffer->StartAccess(bmdBufferAccessWrite); - if (r != S_OK) { - throw std::runtime_error( - "Could not access the video frame byte buffer"); - } - - void *pFrame2 = nullptr; - - r = intermediate_video_buffer->GetBytes((void **)&pFrame2); - if (r != S_OK || !pFrame2) { - throw std::runtime_error( - "Conversion video buffer has no bytes pointer"); - } - pixel_swizzler_.copy_frame_buffer_12bit( - pFrame2, src_buf, num_pix); - - intermediate_video_buffer->EndAccess(bmdBufferAccessWrite); - - // Now we use Decklink's conversion to convert from our intermediate 12 - // bit RGB frame to the desired output format (e.g. 10 bit YUV) - r = frame_converter_->ConvertFrame( - intermediate_frame_, decklink_video_frame); - if (r != S_OK) { - throw std::runtime_error("Failed to convert frame"); - } - } - - } catch (std::exception &e) { - - // reduce log spamming if we're getting errors on every frame - static int error_count = 0; - static std::string last_error; - if (last_error != e.what() || (++error_count) == 120) { - spdlog::error("{} {}", __PRETTY_FUNCTION__, e.what()); - last_error = e.what(); - error_count = 0; - } - } - video_buffer->EndAccess(bmdBufferAccessReadAndWrite); - } - } - } - - try { - - IDeckLinkMutableVideoFrame *mutable_video_buffer = nullptr; - auto r = decklink_video_frame->QueryInterface( - IID_IDeckLinkMutableVideoFrame, (void **)&mutable_video_buffer); - if (r != S_OK || !mutable_video_buffer) { - throw std::runtime_error("Failed to get mutable video buffer"); - } - update_frame_metadata(mutable_video_buffer); - - } catch (std::exception &e) { - // reduce log spamming if we're getting errors on every frame - static int error_count = 0; - static std::string last_error; - if (last_error != e.what() || (++error_count) == 120) { - spdlog::error("{} {}", __PRETTY_FUNCTION__, e.what()); - last_error = e.what(); - error_count = 0; - } - } - - if (video_buffer) - video_buffer->Release(); - - if (decklink_output_interface_->ScheduleVideoFrame( - decklink_video_frame, - (uiTotalFrames * frame_duration_), - frame_duration_, - frame_timescale_) != S_OK) { - mutex_.unlock(); - running_ = false; - decklink_xstudio_plugin_->stop(); - report_error("Failed to schedule video frame."); - return; - } - - if (!running_) { - running_ = true; - report_status(fmt::format("Running in mode {}.", display_mode_name_), running_); - decklink_xstudio_plugin_->start(frameWidth(), frameHeight()); - } - uiTotalFrames++; - mutex_.unlock(); -} - -void DecklinkOutput::update_frame_metadata(IDeckLinkMutableVideoFrame *mutableFrame) { - - if (hdr_metadata_.EOTF == 0) { - // SDR Mode - we don't need to set any metadata, but we do need to make sure to clear - // the HDR flag if it was set by a previous HDR frame - mutableFrame->SetFlags(mutableFrame->GetFlags() & ~bmdFrameContainsHDRMetadata); - return; - } - - mutableFrame->SetFlags(mutableFrame->GetFlags() | bmdFrameContainsHDRMetadata); - - IDeckLinkVideoFrameMutableMetadataExtensions *frameMeta = nullptr; - if (mutableFrame->QueryInterface( - IID_IDeckLinkVideoFrameMutableMetadataExtensions, (void **)&frameMeta) != S_OK) { - // This can fail if the drivers are old and don't support HDR metadata, in which case we - // just won't send any metadata - throw std::runtime_error( - "Failed to get mutable metadata extensions for HDR metadata update."); - } - - frameMeta->AddRef(); - frameMeta->SetInt(bmdDeckLinkFrameMetadataColorspace, hdr_metadata_.colourspace_); - frameMeta->SetInt( - bmdDeckLinkFrameMetadataHDRElectroOpticalTransferFunc, hdr_metadata_.EOTF); - frameMeta->SetFloat( - bmdDeckLinkFrameMetadataHDRDisplayPrimariesRedX, hdr_metadata_.referencePrimaries[0]); - frameMeta->SetFloat( - bmdDeckLinkFrameMetadataHDRDisplayPrimariesRedY, hdr_metadata_.referencePrimaries[1]); - frameMeta->SetFloat( - bmdDeckLinkFrameMetadataHDRDisplayPrimariesGreenX, hdr_metadata_.referencePrimaries[2]); - frameMeta->SetFloat( - bmdDeckLinkFrameMetadataHDRDisplayPrimariesGreenY, hdr_metadata_.referencePrimaries[3]); - frameMeta->SetFloat( - bmdDeckLinkFrameMetadataHDRDisplayPrimariesBlueX, hdr_metadata_.referencePrimaries[4]); - frameMeta->SetFloat( - bmdDeckLinkFrameMetadataHDRDisplayPrimariesBlueY, hdr_metadata_.referencePrimaries[5]); - frameMeta->SetFloat( - bmdDeckLinkFrameMetadataHDRWhitePointX, hdr_metadata_.referencePrimaries[6]); - frameMeta->SetFloat( - bmdDeckLinkFrameMetadataHDRWhitePointY, hdr_metadata_.referencePrimaries[7]); - frameMeta->SetFloat( - bmdDeckLinkFrameMetadataHDRMaxDisplayMasteringLuminance, - hdr_metadata_.luminanceSettings[0]); - frameMeta->SetFloat( - bmdDeckLinkFrameMetadataHDRMinDisplayMasteringLuminance, - hdr_metadata_.luminanceSettings[1]); - frameMeta->SetFloat( - bmdDeckLinkFrameMetadataHDRMaximumContentLightLevel, - hdr_metadata_.luminanceSettings[2]); - frameMeta->SetFloat( - bmdDeckLinkFrameMetadataHDRMaximumFrameAverageLightLevel, - hdr_metadata_.luminanceSettings[3]); - frameMeta->Release(); -} - -void DecklinkOutput::receive_samples_from_xstudio(int16_t *samples, unsigned long num_samps) { - // note this method is called by the xstudio audio output thread in a loop - // that streams chunks of samples to an audio output device (i.e. this class) - // The xstudio audio output actor expects us to return from here only when the - // samples have been 'consumed'. - { - // lock mutex and immediately copy our samples to the buffer ready to - // send to Decklink - std::unique_lock lk2(audio_samples_buf_mutex_); - const size_t buf_size = audio_samples_buffer_.size(); - audio_samples_buffer_.resize(buf_size + num_samps); - memcpy(audio_samples_buffer_.data() + buf_size, samples, num_samps * sizeof(int16_t)); - } - - // now WAIT until the samples have been played (RenderAudioSamples is called - // from a Decklink driver thread - we only want to return from THIS function - // when Decklink needs a top-up of audio samples) - std::unique_lock lk(audio_samples_cv_mutex_); - audio_samples_cv_.wait(lk, [=] { return fetch_more_samples_from_xstudio_; }); - fetch_more_samples_from_xstudio_ = false; -} - -long DecklinkOutput::num_samples_in_buffer() { - - // note this method is called by the xstudio audio output thread - // Have to assume that GetBufferedAudioSampleFrameCount is not thread safe. BMD SDK - // does not tell us otherwise - if (!decklink_output_interface_) { - return 0; - } - std::unique_lock lk0(bmd_mutex_); - uint32_t prerollAudioSampleCount; - if (decklink_output_interface_->GetBufferedAudioSampleFrameCount( - &prerollAudioSampleCount) == S_OK) { - return (long)prerollAudioSampleCount - (audio_sync_delay_milliseconds_ * 48000) / 1000; - } - return 0; -} - -// Note, I have not yet understood the significance of the preroll flag -void DecklinkOutput::copy_audio_samples_to_decklink_buffer(const bool /*preroll*/) { - + + mutex_.lock(); + + free(pFrameBuf); + pFrameBuf = NULL; + + mutex_.unlock(); + + spdlog::info("Stopping Decklink output loop done. {}", error_message); + + return true; +} + +void DecklinkOutput::StartStop() { + if (!running_) + start_sdi_output(); + else + stop_sdi_output(); +} + +void DecklinkOutput::incoming_frame(const media_reader::ImageBufPtr &incoming) { + + // this is called from xstudio managed thread, which is independent of + // the decklink output thread control + frames_mutex_.lock(); + current_frame_ = incoming; + frames_mutex_.unlock(); +} + +namespace { +void multithreadMemCopy(void *_dst, void *_src, size_t buf_size, const int n_threads) { + + // Note: my instinct tells me that spawning threads for + // every copy operation (which might happen 60 times a second) + // is not efficient but it seems that having a threadpool doesn't + // make any real difference, the overhead of thread creation + // is tiny. + std::vector memcpy_threads; + size_t step = ((buf_size / n_threads) / 4096) * 4096; + + uint8_t *dst = (uint8_t *)_dst; + uint8_t *src = (uint8_t *)_src; + + for (int i = 0; i < n_threads; ++i) { + memcpy_threads.emplace_back(memcpy, dst, src, std::min(buf_size, step)); + dst += step; + src += step; + buf_size -= step; + } + + // ensure any threads still running to copy data to this texture are done + for (auto &t : memcpy_threads) { + if (t.joinable()) + t.join(); + } +} + +} // namespace + +#define CHECK_BIT(var, pos) ((var) & (1 << (pos))) + +void DecklinkOutput::report_status( + const std::string &status_message, const bool sdi_output_is_active) { + + utility::JsonStore j; + j["status_message"] = status_message; + j["sdi_output_is_active"] = sdi_output_is_active; + j["error_state"] = false; + decklink_xstudio_plugin_->send_status(j); +} + +void DecklinkOutput::report_error(const std::string &status_message) { + + utility::JsonStore j; + j["status_message"] = status_message; + j["sdi_output_is_active"] = false; + j["error_state"] = true; + decklink_xstudio_plugin_->send_status(j); +} + +void DecklinkOutput::fill_decklink_video_frame(IDeckLinkVideoFrame *decklink_video_frame) { + + // this function (fill_decklink_video_frame) is called by the Decklink API at a steady beat + // matching the refresh rate of the SDI output. We can therefore use it to tell our + // offscreen viewport to render a new frame ready for the subsequent call to this function. + // Remember The frame rendered by xstduio is delivered to us via the incoming_frame callback + // and, as long as xstudio is able to render the video frame in somthing less than the + // refresh period for the SDI output, it should have been delivered before we re-enter this + // function. + // + // The time value passed into this request is our best estimate of when the frame that we + // are requesting will actually be put on the screen. + decklink_xstudio_plugin_->request_video_frame(utility::clock::now()); + + + // We also need to make this crucial call to tell xstudio's offscreen viewport when the + // last video frame was put on screen. It uses the regular beat of these calls to work + // out the refresh rate of the video output and therefore do an accurate 'pulldown' when + // evaluating the playhead position for the next frame to go on screen. + // In the case of the Decklink, we know that this function (fill_decklink_video_frame) is + // being called with a beat matching the SDI refresh (as long as our code immediately below + // completes well inside/ that period) + decklink_xstudio_plugin_->video_frame_consumed(utility::clock::now()); + + static auto tp = utility::clock::now(); + auto tp1 = utility::clock::now(); + tp = tp1; + + mutex_.lock(); + + // SDK v15.3: GetBytes() moved from IDeckLinkVideoFrame to IDeckLinkVideoBuffer + IDeckLinkVideoBuffer *video_buffer = nullptr; + decklink_video_frame->QueryInterface(IID_IDeckLinkVideoBuffer, (void **)&video_buffer); + + frames_mutex_.lock(); + media_reader::ImageBufPtr the_frame = current_frame_; + frames_mutex_.unlock(); + + if (the_frame) { + + auto tp = utility::clock::now(); + + if (the_frame->size() >= decklink_video_frame->GetRowBytes() * frame_height_) { + + int xstudio_buf_pixel_format = the_frame->params().value("pixel_format", 0); + + if (xstudio_buf_pixel_format == ui::viewport::RGBA_16 && video_buffer) { + + // On macOS, DMA buffers may need StartAccess to map into CPU memory + HRESULT sa_hr = video_buffer->StartAccess(bmdBufferAccessReadAndWrite); + try { + + void *pFrame = nullptr; + HRESULT gb_hr = video_buffer->GetBytes((void **)&pFrame); + int num_pix = + decklink_video_frame->GetWidth() * decklink_video_frame->GetHeight(); + void *src_buf = the_frame->buffer(); + + // Validate pointers and ensure source buffer is large enough + // for num_pix RGBA_16 pixels (8 bytes each) + if (!pFrame || !src_buf) { + throw std::runtime_error( + fmt::format( + "Decklink: null buffer in fill_decklink_video_frame " + "(pFrame={}, src_buf={}, num_pix={}, StartAccess=0x{:x}, " + "GetBytes=0x{:x})", + pFrame, + src_buf, + num_pix, + (unsigned long)sa_hr, + (unsigned long)gb_hr)); + } else if (the_frame->size() < (size_t)num_pix * 8) { + throw std::runtime_error( + fmt::format( + "Decklink: source buffer too small " + "(size={}, need={}, frame={}x{}, src_format=RGBA_16)", + the_frame->size(), + (size_t)num_pix * 8, + decklink_video_frame->GetWidth(), + decklink_video_frame->GetHeight())); + } else if (decklink_video_frame->GetPixelFormat() == bmdFormat10BitRGB) { + + // TimeLogger l("RGBA16_to_10bitRGB"); + pixel_swizzler_.copy_frame_buffer_10bit( + pFrame, src_buf, num_pix); + + } else if (decklink_video_frame->GetPixelFormat() == bmdFormat10BitRGBXLE) { + + // TimeLogger l("RGBA16_to_10bitRGBXLE"); + pixel_swizzler_.copy_frame_buffer_10bit( + pFrame, src_buf, num_pix); + + } else if (decklink_video_frame->GetPixelFormat() == bmdFormat10BitRGBX) { + + // TimeLogger l("RGBA16_to_10bitRGBX"); + pixel_swizzler_.copy_frame_buffer_10bit( + pFrame, src_buf, num_pix); + + } else if (decklink_video_frame->GetPixelFormat() == bmdFormat12BitRGB) { + + // TimeLogger l("RGBA16_to_12bitRGB"); + pixel_swizzler_.copy_frame_buffer_12bit( + pFrame, src_buf, num_pix); + + } else if (decklink_video_frame->GetPixelFormat() == bmdFormat12BitRGBLE) { + + // TimeLogger l("RGBA16_to_12bitRGBLE"); + pixel_swizzler_.copy_frame_buffer_12bit( + 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"); + make_intermediate_frame(); + + IDeckLinkVideoBuffer *intermediate_video_buffer = nullptr; + auto r = intermediate_frame_->QueryInterface( + IID_IDeckLinkVideoBuffer, (void **)&intermediate_video_buffer); + if (r != S_OK || !intermediate_video_buffer) { + throw std::runtime_error("Failed to get conversion video buffer"); + } + + r = intermediate_video_buffer->StartAccess(bmdBufferAccessWrite); + if (r != S_OK) { + throw std::runtime_error( + "Could not access the video frame byte buffer"); + } + + void *pFrame2 = nullptr; + + r = intermediate_video_buffer->GetBytes((void **)&pFrame2); + if (r != S_OK || !pFrame2) { + throw std::runtime_error( + "Conversion video buffer has no bytes pointer"); + } + pixel_swizzler_.copy_frame_buffer_12bit( + pFrame2, src_buf, num_pix); + + intermediate_video_buffer->EndAccess(bmdBufferAccessWrite); + + // Now we use Decklink's conversion to convert from our intermediate 12 + // bit RGB frame to the desired output format (e.g. 10 bit YUV) + r = frame_converter_->ConvertFrame( + intermediate_frame_, decklink_video_frame); + if (r != S_OK) { + throw std::runtime_error("Failed to convert frame"); + } + } + + } catch (std::exception &e) { + + // reduce log spamming if we're getting errors on every frame + static int error_count = 0; + static std::string last_error; + if (last_error != e.what() || (++error_count) == 120) { + spdlog::error("{} {}", __PRETTY_FUNCTION__, e.what()); + last_error = e.what(); + error_count = 0; + } + } + video_buffer->EndAccess(bmdBufferAccessReadAndWrite); + } + } + } + + try { + + IDeckLinkMutableVideoFrame *mutable_video_buffer = nullptr; + auto r = decklink_video_frame->QueryInterface( + IID_IDeckLinkMutableVideoFrame, (void **)&mutable_video_buffer); + if (r != S_OK || !mutable_video_buffer) { + throw std::runtime_error("Failed to get mutable video buffer"); + } + update_frame_metadata(mutable_video_buffer); + + } catch (std::exception &e) { + // reduce log spamming if we're getting errors on every frame + static int error_count = 0; + static std::string last_error; + if (last_error != e.what() || (++error_count) == 120) { + spdlog::error("{} {}", __PRETTY_FUNCTION__, e.what()); + last_error = e.what(); + error_count = 0; + } + } + + if (video_buffer) + video_buffer->Release(); + + if (decklink_output_interface_->ScheduleVideoFrame( + decklink_video_frame, + (uiTotalFrames * frame_duration_), + frame_duration_, + frame_timescale_) != S_OK) { + mutex_.unlock(); + running_ = false; + decklink_xstudio_plugin_->stop(); + report_error("Failed to schedule video frame."); + return; + } + + if (!running_) { + running_ = true; + report_status(fmt::format("Running in mode {}.", display_mode_name_), running_); + decklink_xstudio_plugin_->start(frameWidth(), frameHeight()); + } + uiTotalFrames++; + mutex_.unlock(); +} + +void DecklinkOutput::update_frame_metadata(IDeckLinkMutableVideoFrame *mutableFrame) { + + if (hdr_metadata_.EOTF == 0) { + // SDR Mode - we don't need to set any metadata, but we do need to make sure to clear + // the HDR flag if it was set by a previous HDR frame + mutableFrame->SetFlags(mutableFrame->GetFlags() & ~bmdFrameContainsHDRMetadata); + return; + } + + mutableFrame->SetFlags(mutableFrame->GetFlags() | bmdFrameContainsHDRMetadata); + + IDeckLinkVideoFrameMutableMetadataExtensions *frameMeta = nullptr; + if (mutableFrame->QueryInterface( + IID_IDeckLinkVideoFrameMutableMetadataExtensions, (void **)&frameMeta) != S_OK) { + // This can fail if the drivers are old and don't support HDR metadata, in which case we + // just won't send any metadata + throw std::runtime_error( + "Failed to get mutable metadata extensions for HDR metadata update."); + } + + frameMeta->AddRef(); + frameMeta->SetInt(bmdDeckLinkFrameMetadataColorspace, hdr_metadata_.colourspace_); + frameMeta->SetInt( + bmdDeckLinkFrameMetadataHDRElectroOpticalTransferFunc, hdr_metadata_.EOTF); + frameMeta->SetFloat( + bmdDeckLinkFrameMetadataHDRDisplayPrimariesRedX, hdr_metadata_.referencePrimaries[0]); + frameMeta->SetFloat( + bmdDeckLinkFrameMetadataHDRDisplayPrimariesRedY, hdr_metadata_.referencePrimaries[1]); + frameMeta->SetFloat( + bmdDeckLinkFrameMetadataHDRDisplayPrimariesGreenX, hdr_metadata_.referencePrimaries[2]); + frameMeta->SetFloat( + bmdDeckLinkFrameMetadataHDRDisplayPrimariesGreenY, hdr_metadata_.referencePrimaries[3]); + frameMeta->SetFloat( + bmdDeckLinkFrameMetadataHDRDisplayPrimariesBlueX, hdr_metadata_.referencePrimaries[4]); + frameMeta->SetFloat( + bmdDeckLinkFrameMetadataHDRDisplayPrimariesBlueY, hdr_metadata_.referencePrimaries[5]); + frameMeta->SetFloat( + bmdDeckLinkFrameMetadataHDRWhitePointX, hdr_metadata_.referencePrimaries[6]); + frameMeta->SetFloat( + bmdDeckLinkFrameMetadataHDRWhitePointY, hdr_metadata_.referencePrimaries[7]); + frameMeta->SetFloat( + bmdDeckLinkFrameMetadataHDRMaxDisplayMasteringLuminance, + hdr_metadata_.luminanceSettings[0]); + frameMeta->SetFloat( + bmdDeckLinkFrameMetadataHDRMinDisplayMasteringLuminance, + hdr_metadata_.luminanceSettings[1]); + frameMeta->SetFloat( + bmdDeckLinkFrameMetadataHDRMaximumContentLightLevel, + hdr_metadata_.luminanceSettings[2]); + frameMeta->SetFloat( + bmdDeckLinkFrameMetadataHDRMaximumFrameAverageLightLevel, + hdr_metadata_.luminanceSettings[3]); + frameMeta->Release(); +} + +void DecklinkOutput::receive_samples_from_xstudio(int16_t *samples, unsigned long num_samps) { + // note this method is called by the xstudio audio output thread in a loop + // that streams chunks of samples to an audio output device (i.e. this class) + // The xstudio audio output actor expects us to return from here only when the + // samples have been 'consumed'. + { + // lock mutex and immediately copy our samples to the buffer ready to + // send to Decklink + std::unique_lock lk2(audio_samples_buf_mutex_); + const size_t buf_size = audio_samples_buffer_.size(); + audio_samples_buffer_.resize(buf_size + num_samps); + memcpy(audio_samples_buffer_.data() + buf_size, samples, num_samps * sizeof(int16_t)); + } + + // now WAIT until the samples have been played (RenderAudioSamples is called + // from a Decklink driver thread - we only want to return from THIS function + // when Decklink needs a top-up of audio samples) + std::unique_lock lk(audio_samples_cv_mutex_); + audio_samples_cv_.wait(lk, [=] { return fetch_more_samples_from_xstudio_; }); + fetch_more_samples_from_xstudio_ = false; +} + +long DecklinkOutput::num_samples_in_buffer() { + + // note this method is called by the xstudio audio output thread + // Have to assume that GetBufferedAudioSampleFrameCount is not thread safe. BMD SDK + // does not tell us otherwise + if (!decklink_output_interface_) { + return 0; + } + std::unique_lock lk0(bmd_mutex_); + uint32_t prerollAudioSampleCount; + if (decklink_output_interface_->GetBufferedAudioSampleFrameCount( + &prerollAudioSampleCount) == S_OK) { + return (long)prerollAudioSampleCount - (audio_sync_delay_milliseconds_ * 48000) / 1000; + } + return 0; +} + +// Note, I have not yet understood the significance of the preroll flag +void DecklinkOutput::copy_audio_samples_to_decklink_buffer(const bool /*preroll*/) { + if (!decklink_output_interface_) { { std::lock_guard m(audio_samples_cv_mutex_); @@ -1216,62 +1228,62 @@ void DecklinkOutput::copy_audio_samples_to_decklink_buffer(const bool /*preroll* audio_samples_cv_.notify_one(); return; } - - std::unique_lock lk0(bmd_mutex_); - - // How many samples are sitting on the SDI card ready to be played? - uint32_t prerollAudioSampleCount; - if (decklink_output_interface_->GetBufferedAudioSampleFrameCount( - &prerollAudioSampleCount) == S_OK) { - if (prerollAudioSampleCount > samples_water_level_) { - // plenty of samples already in the bmd buffer ready to be played, - // let's do nothing here - return; - } else { - // We need to top-up the samples in the buffer. - - // the xstudio audio output thread is probably waiting in - // receive_samples_from_xstudio ... because the number of samples - // in the BMD buffer is below our target 'water_level' we now - // release the lock in 'receive_samples_from_xstudio' so that the - // xstudio audio sample streaming loop can continue and fetch - // more samples to give to us - { - std::lock_guard m(audio_samples_cv_mutex_); - fetch_more_samples_from_xstudio_ = true; - } - audio_samples_cv_.notify_one(); - } - } - - std::unique_lock lk(audio_samples_buf_mutex_); - - if (audio_samples_buffer_.empty()) { - // 512 samples of silence to start filling buffer in the absence - // of audio samples streaming from xstudio - audio_samples_buffer_.resize(4096); - memset( - audio_samples_buffer_.data(), 0, audio_samples_buffer_.size() * sizeof(uint16_t)); - } - - if (decklink_output_interface_->ScheduleAudioSamples( - audio_samples_buffer_.data(), - audio_samples_buffer_.size() / 2, - samples_delivered_, - bmdAudioSampleRate48kHz, - nullptr) != S_OK) { - throw std::runtime_error("Failed to shedule audio out."); - } - - samples_delivered_ += audio_samples_buffer_.size() / 2; - audio_samples_buffer_.clear(); -} - -//////////////////////////////////////////// -// Render Delegate Class -//////////////////////////////////////////// -AVOutputCallback::AVOutputCallback(DecklinkOutput *pOwner) { owner_ = pOwner; } - + + std::unique_lock lk0(bmd_mutex_); + + // How many samples are sitting on the SDI card ready to be played? + uint32_t prerollAudioSampleCount; + if (decklink_output_interface_->GetBufferedAudioSampleFrameCount( + &prerollAudioSampleCount) == S_OK) { + if (prerollAudioSampleCount > samples_water_level_) { + // plenty of samples already in the bmd buffer ready to be played, + // let's do nothing here + return; + } else { + // We need to top-up the samples in the buffer. + + // the xstudio audio output thread is probably waiting in + // receive_samples_from_xstudio ... because the number of samples + // in the BMD buffer is below our target 'water_level' we now + // release the lock in 'receive_samples_from_xstudio' so that the + // xstudio audio sample streaming loop can continue and fetch + // more samples to give to us + { + std::lock_guard m(audio_samples_cv_mutex_); + fetch_more_samples_from_xstudio_ = true; + } + audio_samples_cv_.notify_one(); + } + } + + std::unique_lock lk(audio_samples_buf_mutex_); + + if (audio_samples_buffer_.empty()) { + // 512 samples of silence to start filling buffer in the absence + // of audio samples streaming from xstudio + audio_samples_buffer_.resize(4096); + memset( + audio_samples_buffer_.data(), 0, audio_samples_buffer_.size() * sizeof(uint16_t)); + } + + if (decklink_output_interface_->ScheduleAudioSamples( + audio_samples_buffer_.data(), + audio_samples_buffer_.size() / 2, + samples_delivered_, + bmdAudioSampleRate48kHz, + nullptr) != S_OK) { + throw std::runtime_error("Failed to shedule audio out."); + } + + samples_delivered_ += audio_samples_buffer_.size() / 2; + audio_samples_buffer_.clear(); +} + +//////////////////////////////////////////// +// Render Delegate Class +//////////////////////////////////////////// +AVOutputCallback::AVOutputCallback(DecklinkOutput *pOwner) { owner_ = pOwner; } + HRESULT AVOutputCallback::QueryInterface(REFIID iid, LPVOID *ppv) { if (!ppv) { return E_INVALIDARG; @@ -1285,6 +1297,53 @@ HRESULT AVOutputCallback::QueryInterface(REFIID iid, LPVOID *ppv) { *ppv = static_cast(static_cast(this)); } else if (std::memcmp(&iid, &IID_IDeckLinkVideoOutputCallback, sizeof(REFIID)) == 0) { *ppv = static_cast(this); + } else { + return E_NOINTERFACE; + } + + AddRef(); + return S_OK; +} + +ULONG AVOutputCallback::AddRef() { + int oldValue; + + oldValue = ref_count_.fetchAndAddAcquire(1); + return (ULONG)(oldValue + 1); +} + +ULONG AVOutputCallback::Release() { + int oldValue; + + oldValue = ref_count_.fetchAndAddAcquire(-1); + if (oldValue == 1) { + delete this; + } + + return (ULONG)(oldValue - 1); +} + +HRESULT AVOutputCallback::ScheduledFrameCompleted( + IDeckLinkVideoFrame *completedFrame, BMDOutputFrameCompletionResult /*result*/) { + owner_->fill_decklink_video_frame(completedFrame); + return S_OK; +} + +HRESULT AVOutputCallback::ScheduledPlaybackHasStopped() { return S_OK; } + +AudioOutputCallback::AudioOutputCallback(DecklinkOutput *pOwner) { owner_ = pOwner; } + +HRESULT AudioOutputCallback::QueryInterface(REFIID iid, LPVOID *ppv) { + if (!ppv) { + return E_INVALIDARG; + } + + *ppv = NULL; + + const auto iid_unknown = IID_IUnknown; + + if (std::memcmp(&iid, &iid_unknown, sizeof(REFIID)) == 0) { + *ppv = static_cast(static_cast(this)); } else if (std::memcmp(&iid, &IID_IDeckLinkAudioOutputCallback, sizeof(REFIID)) == 0) { *ppv = static_cast(this); } else { @@ -1294,37 +1353,29 @@ HRESULT AVOutputCallback::QueryInterface(REFIID iid, LPVOID *ppv) { AddRef(); return S_OK; } - -ULONG AVOutputCallback::AddRef() { - int oldValue; - - oldValue = ref_count_.fetchAndAddAcquire(1); - return (ULONG)(oldValue + 1); -} - -ULONG AVOutputCallback::Release() { - int oldValue; - - oldValue = ref_count_.fetchAndAddAcquire(-1); - if (oldValue == 1) { - delete this; - } - - return (ULONG)(oldValue - 1); -} - -HRESULT AVOutputCallback::ScheduledFrameCompleted( - IDeckLinkVideoFrame *completedFrame, BMDOutputFrameCompletionResult /*result*/) { - owner_->fill_decklink_video_frame(completedFrame); - return S_OK; -} - -HRESULT AVOutputCallback::ScheduledPlaybackHasStopped() { return S_OK; } - -HRESULT AVOutputCallback::RenderAudioSamples(BOOL preroll) { - // decklink driver is calling this at regular intervals. There may be - // plenty of samples in the buffer for it to render, we check that in - // our own function - owner_->copy_audio_samples_to_decklink_buffer(preroll); - return S_OK; -} + +ULONG AudioOutputCallback::AddRef() { + int oldValue; + + oldValue = ref_count_.fetchAndAddAcquire(1); + return (ULONG)(oldValue + 1); +} + +ULONG AudioOutputCallback::Release() { + int oldValue; + + oldValue = ref_count_.fetchAndAddAcquire(-1); + if (oldValue == 1) { + delete this; + } + + return (ULONG)(oldValue - 1); +} + +HRESULT AudioOutputCallback::RenderAudioSamples(BOOL preroll) { + // decklink driver is calling this at regular intervals. There may be + // plenty of samples in the buffer for it to render, we check that in + // our own function + owner_->copy_audio_samples_to_decklink_buffer(preroll); + return S_OK; +} 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 1f44f448d..e239bfb52 100644 --- a/src/plugin/video_output/bmd_decklink/src/decklink_output.hpp +++ b/src/plugin/video_output/bmd_decklink/src/decklink_output.hpp @@ -1,200 +1,201 @@ -// SPDX-License-Identifier: Apache-2.0 -#pragma once - -#ifdef __APPLE__ -#include -#elif defined(_WIN32) -#ifndef NOMINMAX -#define NOMINMAX -#endif -#ifndef WIN32_LEAN_AND_MEAN -#define WIN32_LEAN_AND_MEAN -#endif -#include -#include -#else -#include -#endif -#include -#include -#include -#include -#include - +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#ifdef __APPLE__ +#include +#elif defined(_WIN32) +#ifndef NOMINMAX +#define NOMINMAX +#endif +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#include +#else +#include +#endif +#include +#include +#include +#include +#include + #include "extern/decklink_compat.h" #include "extern/DeckLinkAPI.h" - -#ifndef STDMETHODCALLTYPE -#define STDMETHODCALLTYPE -#endif -#include "xstudio/media_reader/image_buffer.hpp" -#include "pixel_swizzler.hpp" - -namespace xstudio { -namespace bm_decklink_plugin_1_0 { - - class AVOutputCallback; - - struct HDRMetadata { - int64_t EOTF = {0}; - int32_t colourspace_ = {bmdColorspaceRec709}; - std::array referencePrimaries = { - 0.708, 0.292, 0.170, 0.797, 0.131, 0.046, 0.3127, 0.3290}; - std::array luminanceSettings = { - 1000.0, // HDRMaxDisplayMasteringLuminance - 0.0005, // HDRMinDisplayMasteringLuminance - 1000.0, // HDRMaximumContentLightLevel - 400.0 // HDRMaximumFrameAverageLightLevel - }; - }; - - class BMDecklinkPlugin; - - class MockDecklinkOutput { - - public: - MockDecklinkOutput(BMDecklinkPlugin *decklink_xstudio_plugin) {} - - bool init_decklink() { return true; } - - bool start_sdi_output() { return true; } - void set_preroll() {} - bool stop_sdi_output(const std::string &error = std::string()) { return true; } - void StartStop() {} - - void fill_decklink_video_frame(IDeckLinkVideoFrame *decklink_video_frame) {} - void copy_audio_samples_to_decklink_buffer(const bool preroll) {} - void receive_samples_from_xstudio(int16_t *samples, unsigned long num_samps) {} - long num_samples_in_buffer() { return 0; } - void set_display_mode( - const std::string &resolution, - const std::string &refresh_rate, - const BMDPixelFormat pix_format) {} - void set_audio_samples_water_level(const int w) {} - void set_audio_sync_delay_milliseconds(const long ms_delay) {} - - void incoming_frame(const media_reader::ImageBufPtr &frame) {} - - [[nodiscard]] int frameWidth() const { return static_cast(1920); } - [[nodiscard]] int frameHeight() const { return static_cast(1080); } - - std::vector - get_available_refresh_rates(const std::string &output_resolution) const { - return std::vector( - {"23.976", "24", "25", "29.97", "30", "50", "59.94", "60"}); - } - - std::vector output_resolution_names() const { - return std::vector({"1920x1080", "3840x2160"}); - } - - void set_hdr_metadata(const HDRMetadata &) {} - }; - - class DecklinkOutput { - - public: - DecklinkOutput(BMDecklinkPlugin *decklink_xstudio_plugin); - ~DecklinkOutput(); - - static void check_decklink_installation(); - - bool init_decklink(); - void make_intermediate_frame(); - bool start_sdi_output(); - void set_preroll(); - bool stop_sdi_output(const std::string &error = std::string()); - void StartStop(); - - void fill_decklink_video_frame(IDeckLinkVideoFrame *decklink_video_frame); - void update_frame_metadata(IDeckLinkMutableVideoFrame *displayFrame); - void copy_audio_samples_to_decklink_buffer(const bool preroll); - void receive_samples_from_xstudio(int16_t *samples, unsigned long num_samps); - long num_samples_in_buffer(); - void set_display_mode( - const std::string &resolution, - const std::string &refresh_rate, - const BMDPixelFormat pix_format); - void set_audio_samples_water_level(const int w) { samples_water_level_ = (uint32_t)w; } - void set_audio_sync_delay_milliseconds(const long ms_delay) { - audio_sync_delay_milliseconds_ = ms_delay; - } - - void incoming_frame(const media_reader::ImageBufPtr &frame); - - [[nodiscard]] int frameWidth() const { return static_cast(frame_width_); } - [[nodiscard]] int frameHeight() const { return static_cast(frame_height_); } - - std::vector - get_available_refresh_rates(const std::string &output_resolution) const; - - std::vector output_resolution_names() const { - std::vector result; - for (const auto &p : refresh_rate_per_output_resolution_) { - result.push_back(p.first); - } - std::sort(result.begin(), result.end()); - return result; - } - - void set_hdr_metadata(const HDRMetadata &o) { - hdr_metadata_mutex_.lock(); - hdr_metadata_ = o; - hdr_metadata_mutex_.unlock(); - } - + +#ifndef STDMETHODCALLTYPE +#define STDMETHODCALLTYPE +#endif +#include "xstudio/media_reader/image_buffer.hpp" +#include "pixel_swizzler.hpp" + +namespace xstudio { +namespace bm_decklink_plugin_1_0 { + + class AVOutputCallback; + + struct HDRMetadata { + int64_t EOTF = {0}; + int32_t colourspace_ = {bmdColorspaceRec709}; + std::array referencePrimaries = { + 0.708, 0.292, 0.170, 0.797, 0.131, 0.046, 0.3127, 0.3290}; + std::array luminanceSettings = { + 1000.0, // HDRMaxDisplayMasteringLuminance + 0.0005, // HDRMinDisplayMasteringLuminance + 1000.0, // HDRMaximumContentLightLevel + 400.0 // HDRMaximumFrameAverageLightLevel + }; + }; + + class BMDecklinkPlugin; + + class MockDecklinkOutput { + + public: + MockDecklinkOutput(BMDecklinkPlugin *decklink_xstudio_plugin) {} + + bool init_decklink() { return true; } + + bool start_sdi_output() { return true; } + void set_preroll() {} + bool stop_sdi_output(const std::string &error = std::string()) { return true; } + void StartStop() {} + + void fill_decklink_video_frame(IDeckLinkVideoFrame *decklink_video_frame) {} + void copy_audio_samples_to_decklink_buffer(const bool preroll) {} + void receive_samples_from_xstudio(int16_t *samples, unsigned long num_samps) {} + long num_samples_in_buffer() { return 0; } + void set_display_mode( + const std::string &resolution, + const std::string &refresh_rate, + const BMDPixelFormat pix_format) {} + void set_audio_samples_water_level(const int w) {} + void set_audio_sync_delay_milliseconds(const long ms_delay) {} + + void incoming_frame(const media_reader::ImageBufPtr &frame) {} + + [[nodiscard]] int frameWidth() const { return static_cast(1920); } + [[nodiscard]] int frameHeight() const { return static_cast(1080); } + + std::vector + get_available_refresh_rates(const std::string &output_resolution) const { + return std::vector( + {"23.976", "24", "25", "29.97", "30", "50", "59.94", "60"}); + } + + std::vector output_resolution_names() const { + return std::vector({"1920x1080", "3840x2160"}); + } + + void set_hdr_metadata(const HDRMetadata &) {} + }; + + class DecklinkOutput { + + public: + DecklinkOutput(BMDecklinkPlugin *decklink_xstudio_plugin); + ~DecklinkOutput(); + + static void check_decklink_installation(); + + bool init_decklink(); + void make_intermediate_frame(); + bool start_sdi_output(); + void set_preroll(); + bool stop_sdi_output(const std::string &error = std::string()); + void StartStop(); + + void fill_decklink_video_frame(IDeckLinkVideoFrame *decklink_video_frame); + void update_frame_metadata(IDeckLinkMutableVideoFrame *displayFrame); + void copy_audio_samples_to_decklink_buffer(const bool preroll); + void receive_samples_from_xstudio(int16_t *samples, unsigned long num_samps); + long num_samples_in_buffer(); + void set_display_mode( + const std::string &resolution, + const std::string &refresh_rate, + const BMDPixelFormat pix_format); + void set_audio_samples_water_level(const int w) { samples_water_level_ = (uint32_t)w; } + void set_audio_sync_delay_milliseconds(const long ms_delay) { + audio_sync_delay_milliseconds_ = ms_delay; + } + + void incoming_frame(const media_reader::ImageBufPtr &frame); + + [[nodiscard]] int frameWidth() const { return static_cast(frame_width_); } + [[nodiscard]] int frameHeight() const { return static_cast(frame_height_); } + + std::vector + get_available_refresh_rates(const std::string &output_resolution) const; + + std::vector output_resolution_names() const { + std::vector result; + for (const auto &p : refresh_rate_per_output_resolution_) { + result.push_back(p.first); + } + std::sort(result.begin(), result.end()); + return result; + } + + void set_hdr_metadata(const HDRMetadata &o) { + hdr_metadata_mutex_.lock(); + hdr_metadata_ = o; + hdr_metadata_mutex_.unlock(); + } + [[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: - void release_resources(); - void detect_runtime_info(); - void log_runtime_info() const; - - AVOutputCallback *output_callback_ = {nullptr}; - std::mutex mutex_; - - GLenum glStatus = {0}; - GLuint idFrameBuf = {0}, idColorBuf = {0}, idDepthBuf = {0}; - char *pFrameBuf = {nullptr}; - - // DeckLink - uint32_t frame_width_ = {0}; - uint32_t frame_height_ = {0}; - + + private: + void release_resources(); + void detect_runtime_info(); + void log_runtime_info() const; + + AVOutputCallback *video_output_callback_ = {nullptr}; + class AudioOutputCallback *audio_output_callback_ = {nullptr}; + std::mutex mutex_; + + GLenum glStatus = {0}; + GLuint idFrameBuf = {0}, idColorBuf = {0}, idDepthBuf = {0}; + char *pFrameBuf = {nullptr}; + + // DeckLink + uint32_t frame_width_ = {0}; + uint32_t frame_height_ = {0}; + IDeckLink *decklink_interface_ = {nullptr}; IDeckLinkOutput *decklink_output_interface_ = {nullptr}; IDeckLinkVideoConversion *frame_converter_ = {nullptr}; - - 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_; - bool running_ = {false}; - - void query_display_modes(); - - void report_status(const std::string &status_message, bool is_running); - - void report_error(const std::string &status_message); - - std::map> refresh_rate_per_output_resolution_; - std::map, BMDDisplayMode> display_modes_; - - BMDPixelFormat current_pix_format_; - BMDDisplayMode current_display_mode_; - std::string display_mode_name_; - - IDeckLinkMutableVideoFrame *intermediate_frame_ = {nullptr}; - - BMDecklinkPlugin *decklink_xstudio_plugin_; - - std::vector audio_samples_buffer_; - std::mutex audio_samples_buf_mutex_, audio_samples_cv_mutex_, bmd_mutex_; + + 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_; + bool running_ = {false}; + + void query_display_modes(); + + void report_status(const std::string &status_message, bool is_running); + + void report_error(const std::string &status_message); + + std::map> refresh_rate_per_output_resolution_; + std::map, BMDDisplayMode> display_modes_; + + BMDPixelFormat current_pix_format_; + BMDDisplayMode current_display_mode_; + std::string display_mode_name_; + + IDeckLinkMutableVideoFrame *intermediate_frame_ = {nullptr}; + + BMDecklinkPlugin *decklink_xstudio_plugin_; + + std::vector audio_samples_buffer_; + std::mutex audio_samples_buf_mutex_, audio_samples_cv_mutex_, bmd_mutex_; std::condition_variable audio_samples_cv_; bool fetch_more_samples_from_xstudio_ = {false}; unsigned long samples_delivered_ = {0}; @@ -204,7 +205,7 @@ namespace bm_decklink_plugin_1_0 { bool audio_output_enabled_ = {false}; bool scheduled_playback_started_ = {false}; PixelSwizzler pixel_swizzler_; - + HDRMetadata hdr_metadata_; std::mutex hdr_metadata_mutex_; bool is_available_ = {false}; @@ -213,42 +214,67 @@ namespace bm_decklink_plugin_1_0 { std::string runtime_info_ = {}; std::string output_interface_info_ = {}; }; - - class AVOutputCallback : public IDeckLinkVideoOutputCallback, - public IDeckLinkAudioOutputCallback { - private: - struct RefCt { - - int fetchAndAddAcquire(const int delta) { - - std::lock_guard l(m); - int old = count; - count += delta; - return old; - } - std::atomic count = 1; - std::mutex m; - } ref_count_; - - DecklinkOutput *owner_; - - public: - AVOutputCallback(DecklinkOutput *pOwner); - - // IUnknown - HRESULT QueryInterface(REFIID /*iid*/, LPVOID * /*ppv*/) override; - ULONG AddRef() override; - ULONG Release() override; - - // IDeckLinkAudioOutputCallback - HRESULT RenderAudioSamples(BOOL preroll) override; - - // IDeckLinkVideoOutputCallback + + class AVOutputCallback : public IDeckLinkVideoOutputCallback { + private: + struct RefCt { + + int fetchAndAddAcquire(const int delta) { + + std::lock_guard l(m); + int old = count; + count += delta; + return old; + } + std::atomic count = 1; + std::mutex m; + } ref_count_; + + DecklinkOutput *owner_; + + public: + AVOutputCallback(DecklinkOutput *pOwner); + + // IUnknown + HRESULT QueryInterface(REFIID /*iid*/, LPVOID * /*ppv*/) override; + ULONG AddRef() override; + ULONG Release() override; + + // IDeckLinkVideoOutputCallback HRESULT ScheduledFrameCompleted( IDeckLinkVideoFrame *completedFrame, BMDOutputFrameCompletionResult result) override; HRESULT ScheduledPlaybackHasStopped() override; }; - -} // namespace bm_decklink_plugin_1_0 -} // namespace xstudio + + class AudioOutputCallback : public IDeckLinkAudioOutputCallback { + private: + struct RefCt { + + int fetchAndAddAcquire(const int delta) { + + std::lock_guard l(m); + int old = count; + count += delta; + return old; + } + std::atomic count = 1; + std::mutex m; + } ref_count_; + + DecklinkOutput *owner_; + + public: + AudioOutputCallback(DecklinkOutput *pOwner); + + // IUnknown + HRESULT QueryInterface(REFIID /*iid*/, LPVOID * /*ppv*/) override; + ULONG AddRef() override; + ULONG Release() override; + + // IDeckLinkAudioOutputCallback + HRESULT RenderAudioSamples(BOOL preroll) override; + }; + +} // namespace bm_decklink_plugin_1_0 +} // namespace xstudio From cc034876d92f3135d749684f4f6dbd34c5aac14d Mon Sep 17 00:00:00 2001 From: "florence.beliveau" Date: Fri, 3 Apr 2026 13:40:44 +0200 Subject: [PATCH 10/12] Require DeckLink Desktop Video 15.x on Linux --- .../bmd_decklink/src/decklink_output.cpp | 61 ++++++------------- 1 file changed, 17 insertions(+), 44 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 932deb021..1ef279dee 100644 --- a/src/plugin/video_output/bmd_decklink/src/decklink_output.cpp +++ b/src/plugin/video_output/bmd_decklink/src/decklink_output.cpp @@ -427,58 +427,31 @@ bool DecklinkOutput::init_decklink() { "DeckLink drivers found but no device is installed", runtime_info_)); } + DeckLinkVersion parsed_version; + const DeckLinkVersion minimum_supported{15, 0, 0}; + const bool parsed = parse_decklink_version(api_version_, parsed_version); + if (parsed && is_decklink_version_older_than(parsed_version, minimum_supported)) { + const auto upgrade_message = with_runtime_details( + "Unsupported Blackmagic DeckLink runtime detected. xStudio requires " + "Blackmagic Desktop Video drivers 15.x or newer to use Blackmagic cards.", + runtime_info_); + spdlog::error("{}", upgrade_message); + spdlog::error( + "Upgrade Blackmagic Desktop Video drivers to version 15.x or newer to " + "enable Blackmagic card support."); + throw std::runtime_error(upgrade_message); + } + const auto modern_output_result = decklink_interface_->QueryInterface( IID_IDeckLinkOutput, (void **)&decklink_output_interface_); if (modern_output_result != S_OK) { spdlog::warn( "DeckLink modern output interface query failed with {}.", format_hresult(modern_output_result)); -#ifdef __linux__ - IDeckLinkOutput_v14_2_1 *legacy_output_interface = nullptr; - const auto legacy_output_result = decklink_interface_->QueryInterface( - IID_IDeckLinkOutput_v14_2_1, (void **)&legacy_output_interface); - if (legacy_output_result == S_OK && legacy_output_interface != nullptr) { - output_interface_info_ = "IID_IDeckLinkOutput_v14_2_1"; - spdlog::warn( - "DeckLink legacy v14.2.1 output interface query succeeded with {}.", - format_hresult(legacy_output_result)); - DeckLinkVersion parsed_version; - const DeckLinkVersion minimum_supported{14, 2, 1}; - const bool parsed = parse_decklink_version(api_version_, parsed_version); - if (!parsed || is_decklink_version_older_than(parsed_version, minimum_supported)) { - legacy_output_interface->Release(); - const auto upgrade_message = with_runtime_details( - "Unsupported Blackmagic DeckLink Linux runtime detected. Drivers " - "older than 14.2.1 are not supported. Upgrade Blackmagic Desktop " - "Video drivers to use Blackmagic cards in xStudio.", - runtime_info_); - spdlog::error("{}", upgrade_message); - spdlog::error( - "Upgrade Blackmagic Desktop Video drivers to version 14.2.1 or " - "newer to enable Blackmagic card support on Linux."); - throw std::runtime_error(upgrade_message); - } - - decklink_output_interface_ = - reinterpret_cast(legacy_output_interface); - spdlog::warn( - "DeckLink output is using the Linux v14.2.1 compatibility ABI. " - "Upgrade Blackmagic Desktop Video drivers to 15.x or newer to use the " - "modern DeckLink binaries."); - } else { - spdlog::error( - "DeckLink legacy v14.2.1 output interface query failed with {}.", - format_hresult(legacy_output_result)); - 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", + "Unsupported Blackmagic DeckLink runtime detected. xStudio requires " + "Blackmagic Desktop Video drivers 15.x or newer to use Blackmagic cards.", runtime_info_)); -#endif } else { output_interface_info_ = "IID_IDeckLinkOutput"; spdlog::info( From 7e2e173a5ab5c5cacdad3563fa4b134b394cfaca Mon Sep 17 00:00:00 2001 From: "florence.beliveau" Date: Fri, 3 Apr 2026 15:03:53 +0200 Subject: [PATCH 11/12] Harden DeckLink callback and runtime cleanup --- .../bmd_decklink/src/decklink_output.cpp | 109 ++++++++++-------- 1 file changed, 64 insertions(+), 45 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 1ef279dee..575475f05 100644 --- a/src/plugin/video_output/bmd_decklink/src/decklink_output.cpp +++ b/src/plugin/video_output/bmd_decklink/src/decklink_output.cpp @@ -11,11 +11,7 @@ #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; @@ -110,15 +106,6 @@ void DecklinkOutput::detect_runtime_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()) { @@ -245,6 +232,9 @@ void DecklinkOutput::release_resources() { if (decklink_output_interface_ != NULL) { spdlog::info("Stopping Decklink output loop."); + decklink_output_interface_->SetScheduledFrameCompletionCallback(nullptr); + decklink_output_interface_->SetAudioCallback(nullptr); + if (scheduled_playback_started_) { decklink_output_interface_->StopScheduledPlayback(0, NULL, 0); scheduled_playback_started_ = false; @@ -430,7 +420,7 @@ bool DecklinkOutput::init_decklink() { DeckLinkVersion parsed_version; const DeckLinkVersion minimum_supported{15, 0, 0}; const bool parsed = parse_decklink_version(api_version_, parsed_version); - if (parsed && is_decklink_version_older_than(parsed_version, minimum_supported)) { + if (!parsed || is_decklink_version_older_than(parsed_version, minimum_supported)) { const auto upgrade_message = with_runtime_details( "Unsupported Blackmagic DeckLink runtime detected. xStudio requires " "Blackmagic Desktop Video drivers 15.x or newer to use Blackmagic cards.", @@ -802,7 +792,7 @@ bool DecklinkOutput::stop_sdi_output(const std::string &error_message) { } audio_samples_cv_.notify_all(); - mutex_.lock(); + std::unique_lock output_lock(mutex_); free(pFrameBuf); pFrameBuf = NULL; @@ -909,15 +899,17 @@ void DecklinkOutput::fill_decklink_video_frame(IDeckLinkVideoFrame *decklink_vid auto tp1 = utility::clock::now(); tp = tp1; - mutex_.lock(); + std::unique_lock output_lock(mutex_); // SDK v15.3: GetBytes() moved from IDeckLinkVideoFrame to IDeckLinkVideoBuffer IDeckLinkVideoBuffer *video_buffer = nullptr; decklink_video_frame->QueryInterface(IID_IDeckLinkVideoBuffer, (void **)&video_buffer); - frames_mutex_.lock(); - media_reader::ImageBufPtr the_frame = current_frame_; - frames_mutex_.unlock(); + media_reader::ImageBufPtr the_frame; + { + std::lock_guard frame_lock(frames_mutex_); + the_frame = current_frame_; + } if (the_frame) { @@ -931,6 +923,8 @@ void DecklinkOutput::fill_decklink_video_frame(IDeckLinkVideoFrame *decklink_vid // On macOS, DMA buffers may need StartAccess to map into CPU memory HRESULT sa_hr = video_buffer->StartAccess(bmdBufferAccessReadAndWrite); + IDeckLinkVideoBuffer *intermediate_video_buffer = nullptr; + bool intermediate_access_started = false; try { void *pFrame = nullptr; @@ -1001,7 +995,6 @@ void DecklinkOutput::fill_decklink_video_frame(IDeckLinkVideoFrame *decklink_vid // TimeLogger l("RGBA16_to_12bitRGBLE"); make_intermediate_frame(); - IDeckLinkVideoBuffer *intermediate_video_buffer = nullptr; auto r = intermediate_frame_->QueryInterface( IID_IDeckLinkVideoBuffer, (void **)&intermediate_video_buffer); if (r != S_OK || !intermediate_video_buffer) { @@ -1013,6 +1006,7 @@ void DecklinkOutput::fill_decklink_video_frame(IDeckLinkVideoFrame *decklink_vid throw std::runtime_error( "Could not access the video frame byte buffer"); } + intermediate_access_started = true; void *pFrame2 = nullptr; @@ -1024,8 +1018,6 @@ void DecklinkOutput::fill_decklink_video_frame(IDeckLinkVideoFrame *decklink_vid pixel_swizzler_.copy_frame_buffer_12bit( pFrame2, src_buf, num_pix); - intermediate_video_buffer->EndAccess(bmdBufferAccessWrite); - // Now we use Decklink's conversion to convert from our intermediate 12 // bit RGB frame to the desired output format (e.g. 10 bit YUV) r = frame_converter_->ConvertFrame( @@ -1046,20 +1038,28 @@ void DecklinkOutput::fill_decklink_video_frame(IDeckLinkVideoFrame *decklink_vid error_count = 0; } } + if (intermediate_video_buffer) { + if (intermediate_access_started) { + intermediate_video_buffer->EndAccess(bmdBufferAccessWrite); + } + intermediate_video_buffer->Release(); + } video_buffer->EndAccess(bmdBufferAccessReadAndWrite); } } } + IDeckLinkMutableVideoFrame *mutable_video_buffer = nullptr; try { - IDeckLinkMutableVideoFrame *mutable_video_buffer = nullptr; auto r = decklink_video_frame->QueryInterface( IID_IDeckLinkMutableVideoFrame, (void **)&mutable_video_buffer); if (r != S_OK || !mutable_video_buffer) { throw std::runtime_error("Failed to get mutable video buffer"); } update_frame_metadata(mutable_video_buffer); + mutable_video_buffer->Release(); + mutable_video_buffer = nullptr; } catch (std::exception &e) { // reduce log spamming if we're getting errors on every frame @@ -1071,6 +1071,9 @@ void DecklinkOutput::fill_decklink_video_frame(IDeckLinkVideoFrame *decklink_vid error_count = 0; } } + if (mutable_video_buffer) { + mutable_video_buffer->Release(); + } if (video_buffer) video_buffer->Release(); @@ -1080,7 +1083,6 @@ void DecklinkOutput::fill_decklink_video_frame(IDeckLinkVideoFrame *decklink_vid (uiTotalFrames * frame_duration_), frame_duration_, frame_timescale_) != S_OK) { - mutex_.unlock(); running_ = false; decklink_xstudio_plugin_->stop(); report_error("Failed to schedule video frame."); @@ -1093,12 +1095,16 @@ void DecklinkOutput::fill_decklink_video_frame(IDeckLinkVideoFrame *decklink_vid decklink_xstudio_plugin_->start(frameWidth(), frameHeight()); } uiTotalFrames++; - mutex_.unlock(); } void DecklinkOutput::update_frame_metadata(IDeckLinkMutableVideoFrame *mutableFrame) { + HDRMetadata hdr_metadata; + { + std::lock_guard lock(hdr_metadata_mutex_); + hdr_metadata = hdr_metadata_; + } - if (hdr_metadata_.EOTF == 0) { + if (hdr_metadata.EOTF == 0) { // SDR Mode - we don't need to set any metadata, but we do need to make sure to clear // the HDR flag if it was set by a previous HDR frame mutableFrame->SetFlags(mutableFrame->GetFlags() & ~bmdFrameContainsHDRMetadata); @@ -1116,38 +1122,37 @@ void DecklinkOutput::update_frame_metadata(IDeckLinkMutableVideoFrame *mutableFr "Failed to get mutable metadata extensions for HDR metadata update."); } - frameMeta->AddRef(); - frameMeta->SetInt(bmdDeckLinkFrameMetadataColorspace, hdr_metadata_.colourspace_); + frameMeta->SetInt(bmdDeckLinkFrameMetadataColorspace, hdr_metadata.colourspace_); frameMeta->SetInt( - bmdDeckLinkFrameMetadataHDRElectroOpticalTransferFunc, hdr_metadata_.EOTF); + bmdDeckLinkFrameMetadataHDRElectroOpticalTransferFunc, hdr_metadata.EOTF); frameMeta->SetFloat( - bmdDeckLinkFrameMetadataHDRDisplayPrimariesRedX, hdr_metadata_.referencePrimaries[0]); + bmdDeckLinkFrameMetadataHDRDisplayPrimariesRedX, hdr_metadata.referencePrimaries[0]); frameMeta->SetFloat( - bmdDeckLinkFrameMetadataHDRDisplayPrimariesRedY, hdr_metadata_.referencePrimaries[1]); + bmdDeckLinkFrameMetadataHDRDisplayPrimariesRedY, hdr_metadata.referencePrimaries[1]); frameMeta->SetFloat( - bmdDeckLinkFrameMetadataHDRDisplayPrimariesGreenX, hdr_metadata_.referencePrimaries[2]); + bmdDeckLinkFrameMetadataHDRDisplayPrimariesGreenX, hdr_metadata.referencePrimaries[2]); frameMeta->SetFloat( - bmdDeckLinkFrameMetadataHDRDisplayPrimariesGreenY, hdr_metadata_.referencePrimaries[3]); + bmdDeckLinkFrameMetadataHDRDisplayPrimariesGreenY, hdr_metadata.referencePrimaries[3]); frameMeta->SetFloat( - bmdDeckLinkFrameMetadataHDRDisplayPrimariesBlueX, hdr_metadata_.referencePrimaries[4]); + bmdDeckLinkFrameMetadataHDRDisplayPrimariesBlueX, hdr_metadata.referencePrimaries[4]); frameMeta->SetFloat( - bmdDeckLinkFrameMetadataHDRDisplayPrimariesBlueY, hdr_metadata_.referencePrimaries[5]); + bmdDeckLinkFrameMetadataHDRDisplayPrimariesBlueY, hdr_metadata.referencePrimaries[5]); frameMeta->SetFloat( - bmdDeckLinkFrameMetadataHDRWhitePointX, hdr_metadata_.referencePrimaries[6]); + bmdDeckLinkFrameMetadataHDRWhitePointX, hdr_metadata.referencePrimaries[6]); frameMeta->SetFloat( - bmdDeckLinkFrameMetadataHDRWhitePointY, hdr_metadata_.referencePrimaries[7]); + bmdDeckLinkFrameMetadataHDRWhitePointY, hdr_metadata.referencePrimaries[7]); frameMeta->SetFloat( bmdDeckLinkFrameMetadataHDRMaxDisplayMasteringLuminance, - hdr_metadata_.luminanceSettings[0]); + hdr_metadata.luminanceSettings[0]); frameMeta->SetFloat( bmdDeckLinkFrameMetadataHDRMinDisplayMasteringLuminance, - hdr_metadata_.luminanceSettings[1]); + hdr_metadata.luminanceSettings[1]); frameMeta->SetFloat( bmdDeckLinkFrameMetadataHDRMaximumContentLightLevel, - hdr_metadata_.luminanceSettings[2]); + hdr_metadata.luminanceSettings[2]); frameMeta->SetFloat( bmdDeckLinkFrameMetadataHDRMaximumFrameAverageLightLevel, - hdr_metadata_.luminanceSettings[3]); + hdr_metadata.luminanceSettings[3]); frameMeta->Release(); } @@ -1298,8 +1303,15 @@ ULONG AVOutputCallback::Release() { HRESULT AVOutputCallback::ScheduledFrameCompleted( IDeckLinkVideoFrame *completedFrame, BMDOutputFrameCompletionResult /*result*/) { - owner_->fill_decklink_video_frame(completedFrame); - return S_OK; + try { + owner_->fill_decklink_video_frame(completedFrame); + return S_OK; + } catch (const std::exception &e) { + spdlog::error("DeckLink video callback failed: {}", e.what()); + } catch (...) { + spdlog::error("DeckLink video callback failed with an unknown exception."); + } + return E_FAIL; } HRESULT AVOutputCallback::ScheduledPlaybackHasStopped() { return S_OK; } @@ -1349,6 +1361,13 @@ HRESULT AudioOutputCallback::RenderAudioSamples(BOOL preroll) { // decklink driver is calling this at regular intervals. There may be // plenty of samples in the buffer for it to render, we check that in // our own function - owner_->copy_audio_samples_to_decklink_buffer(preroll); - return S_OK; + try { + owner_->copy_audio_samples_to_decklink_buffer(preroll); + return S_OK; + } catch (const std::exception &e) { + spdlog::error("DeckLink audio callback failed: {}", e.what()); + } catch (...) { + spdlog::error("DeckLink audio callback failed with an unknown exception."); + } + return E_FAIL; } From 326793f8110fbe1cf7a5c813eb1f5b330c1cf2e6 Mon Sep 17 00:00:00 2001 From: xShirae Date: Fri, 3 Apr 2026 15:37:20 +0200 Subject: [PATCH 12/12] Trim DeckLink unsupported runtime logging --- .../bmd_decklink/src/decklink_output.cpp | 4 - .../bmd_decklink/src/decklink_plugin.cpp | 1214 ++++++++--------- .../src/extern/linux/DeckLinkAPIDispatch.cpp | 480 ++++--- 3 files changed, 844 insertions(+), 854 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 575475f05..8dab0838b 100644 --- a/src/plugin/video_output/bmd_decklink/src/decklink_output.cpp +++ b/src/plugin/video_output/bmd_decklink/src/decklink_output.cpp @@ -426,9 +426,6 @@ bool DecklinkOutput::init_decklink() { "Blackmagic Desktop Video drivers 15.x or newer to use Blackmagic cards.", runtime_info_); spdlog::error("{}", upgrade_message); - spdlog::error( - "Upgrade Blackmagic Desktop Video drivers to version 15.x or newer to " - "enable Blackmagic card support."); throw std::runtime_error(upgrade_message); } @@ -524,7 +521,6 @@ bool DecklinkOutput::init_decklink() { is_available_ = false; last_error_ = e.what(); - std::cerr << "DecklinkOutput::init_decklink() failed: " << e.what() << "\n"; report_error(e.what()); if (decklink_output_interface_) { 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 cc65e4044..bc624d996 100644 --- a/src/plugin/video_output/bmd_decklink/src/decklink_plugin.cpp +++ b/src/plugin/video_output/bmd_decklink/src/decklink_plugin.cpp @@ -1,614 +1,614 @@ -// SPDX-License-Identifier: Apache-2.0 -#ifdef __APPLE__ -#include -#else -#undef WIN32_LEAN_AND_MEAN -#include -#include -#endif - -#include - -#include - -#include "decklink_audio_device.hpp" -#include "decklink_plugin.hpp" -#include "decklink_output.hpp" - -#include "xstudio/utility/helpers.hpp" -#include "xstudio/utility/logging.hpp" - -#include -#include - -#ifdef _WIN32 -#include -#endif - -using namespace xstudio; -using namespace xstudio::ui; -using namespace xstudio::bm_decklink_plugin_1_0; - -namespace { -static std::map bmd_pixel_formats( - {{"8 bit YUV", bmdFormat8BitYUV}, - {"10 bit YUV", bmdFormat10BitYUV}, - {"8 bit ARGB", bmdFormat8BitARGB}, - {"8 bit BGRA", bmdFormat8BitBGRA}, - {"10 bit RGB", bmdFormat10BitRGB}, - {"12 bit RGB", bmdFormat12BitRGB}, - {"12 bit RGB-LE", bmdFormat12BitRGBLE}, - {"10 bit RGB Video Range", bmdFormat10BitRGBX}, - {"10 bit RGB-LE Video Range", bmdFormat10BitRGBXLE}}); - -static const std::vector hdr_metadata_value_names( - {"Red x", "Red y", "Green x", "Green y", "Blue x", "Blue y", "White Pt. x", "White Pt. y"}); - -const std::vector - light_level_controls_names({"D.M. Lum. Min", "D.M. Lum. Max", "CLL Max", "Frm Avg Light"}); - -const std::map eotf_modes({ - {"SDR", 0}, - {"HDR", 1}, - {"PQ", 2}, - {"HLG", 3}, - {"Auto PQ", -1}, -}); - -const std::map colourspaces( - {{"BT. 601", bmdColorspaceRec601}, - {"BT. 709", bmdColorspaceRec709}, - {"BT. 2020", bmdColorspaceRec2020}}); - -static const std::string shelf_button_qml(R"( -import QtQuick 2.12 -import BlackmagicSDI 1.0 -DecklinkShelfButton { -} -)"); -} // namespace - -BMDecklinkPlugin::BMDecklinkPlugin( - caf::actor_config &cfg, const utility::JsonStore &init_settings) - : ui::viewport::VideoOutputPlugin(cfg, init_settings, "BMDecklinkPlugin") { - - // Here we check that the Decklink drivers are installed. If not, we log a - // message and disable the plugin by throwing an exception in the constructor - // which will prevent the plugin from being created. This prevents the SDI - // UI component from appearing in xSTUDIO and confusing the user when they - // don't have a DeckLink device or drivers installed. - try { - - DecklinkOutput::check_decklink_installation(); - - } catch (const std::exception &e) { - - send_exit(this, caf::exit_reason::user_shutdown); - spdlog::info("Blackmagic Decklink SDI output disabled: {}", e.what()); - return; - } - - // add attributes used for configuring the SDI output - pixel_formats_ = add_string_choice_attribute( - "Pixel Format", "Pix Fmt", "10 bit YUV", utility::map_key_to_vec(bmd_pixel_formats)); - pixel_formats_->expose_in_ui_attrs_group("Decklink Settings"); - pixel_formats_->set_preference_path("/plugin/decklink/pixel_format"); - - sdi_output_is_running_ = add_boolean_attribute("Enabled", "Enabled", false); - sdi_output_is_running_->expose_in_ui_attrs_group("Decklink Settings"); - - status_message_ = add_string_attribute("Status", "Status", "Not Started"); - status_message_->expose_in_ui_attrs_group("Decklink Settings"); - - is_in_error_ = add_boolean_attribute("In Error", "In Error", false); - is_in_error_->expose_in_ui_attrs_group("Decklink Settings"); - - track_main_viewport_ = add_boolean_attribute("Track Viewport", "Track Viewport", true); - track_main_viewport_->expose_in_ui_attrs_group("Decklink Settings"); - - resolutions_ = add_string_choice_attribute("Output Resolution", "Out. Res."); - - resolutions_->expose_in_ui_attrs_group("Decklink Settings"); - resolutions_->set_preference_path("/plugin/decklink/output_resolution"); - - frame_rates_ = add_string_choice_attribute("Refresh Rate", "Hz"); - frame_rates_->expose_in_ui_attrs_group("Decklink Settings"); - frame_rates_->set_preference_path("/plugin/decklink/output_frame_rate"); - - start_stop_ = add_boolean_attribute("Start Stop", "Start Stop", false); - start_stop_->expose_in_ui_attrs_group("Decklink Settings"); - - auto_start_ = add_boolean_attribute("Auto Start", "Auto Start", false); - auto_start_->set_preference_path("/plugin/decklink/auto_start_sdi"); - - samples_water_level_ = - add_integer_attribute("Audio Samples Water Level", "Audio Samples Water Level", 4096); - samples_water_level_->set_preference_path("/plugin/decklink/audio_samps_water_level"); - - // This attr is currently not exposed to the user. It can be used to delay (or advance) the - // image that is sent to the SDI output vs. the main xSTUDIO viewport in order to exactly - // sync up the image that shows in xSTUDIO UI vs. the SDI display. This helps if you have a - // screening room with xSTUDIO UI on a monitor and the SDI output shown by a projector at - // the front of the room ... the person operating xSTUDIO may see the image on their screen - // is a few frames ahead of the SDI. However, for this to work we have to buffer a lot more - // frames in the playhead and this is expensive which is why I'm leaving it here but not - // exposing it. - video_pipeline_delay_milliseconds_ = - add_integer_attribute("Video Sync Delay", "Video Sync Delay", 0); - video_pipeline_delay_milliseconds_->set_preference_path( - "/plugin/decklink/video_sync_delay"); - video_pipeline_delay_milliseconds_->expose_in_ui_attrs_group("Decklink Settings"); - - audio_sync_delay_milliseconds_ = - add_integer_attribute("Audio Sync Delay", "Audio Sync Delay", 0); - audio_sync_delay_milliseconds_->set_preference_path( - "/plugin/decklink/new_audio_sync_delay"); - audio_sync_delay_milliseconds_->expose_in_ui_attrs_group("Decklink Settings"); - - disable_pc_audio_when_running_ = - add_boolean_attribute("Auto Disable PC Audio", "Auto Disable PC Audio", false); - disable_pc_audio_when_running_->set_preference_path( - "/plugin/decklink/disable_pc_audio_when_sdi_is_running"); - disable_pc_audio_when_running_->expose_in_ui_attrs_group("Decklink Settings"); - - make_hdr_attributes(); - - VideoOutputPlugin::finalise(); -} - -// This method is called when a new image buffer is ready to be displayed -void BMDecklinkPlugin::incoming_video_frame_callback(media_reader::ImageBufPtr incoming) { - if (dcl_output_ && dcl_output_->is_available()) { - dcl_output_->incoming_frame(incoming); - } -} - -void BMDecklinkPlugin::exit_cleanup() { - // the dcl_output_ has caf::actor handles pointing to this (BMDecklinkPlugin) - // 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) { - - if (status_data.contains("status_message") && status_data["status_message"].is_string()) { - status_message_->set_value(status_data["status_message"].get()); - } - if (status_data.contains("sdi_output_is_active") && - status_data["sdi_output_is_active"].is_boolean()) { - sdi_output_is_running_->set_value(status_data["sdi_output_is_active"].get()); - } - if (status_data.contains("error_state") && status_data["error_state"].is_boolean()) { - is_in_error_->set_value(status_data["error_state"].get()); - } -} - -void BMDecklinkPlugin::attribute_changed(const utility::Uuid &attribute_uuid, const int role) { - - if (dcl_output_) { - - if (resolutions_ && attribute_uuid == resolutions_->uuid() && - role == module::Attribute::Value) { - - const auto rates = dcl_output_->get_available_refresh_rates(resolutions_->value()); - frame_rates_->set_role_data(module::Attribute::StringChoices, rates); - - // pick a sensible refresh rate, if the current rate isn't available for new - // resolution - auto i = std::find( - rates.begin(), rates.end(), frame_rates_->value()); // prefer current rate - if (i == rates.end()) { - i = std::find(rates.begin(), rates.end(), "24.0"); // otherwise prefer 24.0 - if (i == rates.end()) { - i = std::find(rates.begin(), rates.end(), "60.0"); // use 60.0 otherwise - if (i == rates.end()) { - i = rates.begin(); - } - } - } - if (i != rates.end()) { - frame_rates_->set_value(*i); - } - - } else if (attribute_uuid == start_stop_->uuid()) { - - dcl_output_->StartStop(); - } - - if (attribute_uuid == pixel_formats_->uuid() || - attribute_uuid == resolutions_->uuid() || attribute_uuid == frame_rates_->uuid()) { - - try { - - if (bmd_pixel_formats.find(pixel_formats_->value()) == - bmd_pixel_formats.end()) { - throw std::runtime_error( - fmt::format("Invalid pixel format: {}", pixel_formats_->value())); - } - - const BMDPixelFormat pix_fmt = bmd_pixel_formats[pixel_formats_->value()]; - - - dcl_output_->set_display_mode( - resolutions_->value(), frame_rates_->value(), pix_fmt); - - } catch (std::exception &e) { - - status_message_->set_value(e.what()); - is_in_error_->set_value(true); - } - - } else if (attribute_uuid == track_main_viewport_->uuid()) { - - sync_geometry_to_main_viewport(track_main_viewport_->value()); - - } else if (attribute_uuid == samples_water_level_->uuid()) { - dcl_output_->set_audio_samples_water_level(samples_water_level_->value()); - } else if (attribute_uuid == audio_sync_delay_milliseconds_->uuid()) { - dcl_output_->set_audio_sync_delay_milliseconds( - audio_sync_delay_milliseconds_->value()); - } else if (attribute_uuid == video_pipeline_delay_milliseconds_->uuid()) { - video_delay_milliseconds(video_pipeline_delay_milliseconds_->value()); - } else if (attribute_uuid == disable_pc_audio_when_running_->uuid()) { - set_pc_audio_muting(); - } else if (attribute_uuid == sdi_output_is_running_->uuid()) { - set_pc_audio_muting(); - } else if (attribute_uuid == hdr_presets_->uuid()) { - - if (hdr_presets_data_.contains(hdr_presets_->value())) { - - const auto &vals = hdr_presets_data_[hdr_presets_->value()]; - if (vals.is_array() && vals.size() == 8) { - int idx = 0; - for (auto &setting_attr : hdr_metadata_settings_) { - setting_attr->set_value(vals[idx++].get()); - } - } - } - - } else if ( - role == module::Attribute::Value && - hdr_metadata_settings_uuids_.find(attribute_uuid) != - hdr_metadata_settings_uuids_.end()) { - set_hdr_mode_and_metadata(); - if (!prefs_save_scheduled_) { - prefs_save_scheduled_ = true; - delayed_anon_send( - caf::actor_cast(this), - std::chrono::seconds(5), - "save_hdr_colour_prefs"); - } - } - } - StandardPlugin::attribute_changed(attribute_uuid, role); -} - -audio::AudioOutputDevice * -BMDecklinkPlugin::make_audio_output_device(const utility::JsonStore &prefs) { - if (!dcl_output_ || !dcl_output_->is_available()) { - return nullptr; - } - return static_cast( - new DecklinkAudioOutputDevice(prefs, dcl_output_)); -} - -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(); +// SPDX-License-Identifier: Apache-2.0 +#ifdef __APPLE__ +#include +#else +#undef WIN32_LEAN_AND_MEAN +#include +#include +#endif + +#include + +#include + +#include "decklink_audio_device.hpp" +#include "decklink_plugin.hpp" +#include "decklink_output.hpp" + +#include "xstudio/utility/helpers.hpp" +#include "xstudio/utility/logging.hpp" + +#include +#include + +#ifdef _WIN32 +#include +#endif + +using namespace xstudio; +using namespace xstudio::ui; +using namespace xstudio::bm_decklink_plugin_1_0; + +namespace { +static std::map bmd_pixel_formats( + {{"8 bit YUV", bmdFormat8BitYUV}, + {"10 bit YUV", bmdFormat10BitYUV}, + {"8 bit ARGB", bmdFormat8BitARGB}, + {"8 bit BGRA", bmdFormat8BitBGRA}, + {"10 bit RGB", bmdFormat10BitRGB}, + {"12 bit RGB", bmdFormat12BitRGB}, + {"12 bit RGB-LE", bmdFormat12BitRGBLE}, + {"10 bit RGB Video Range", bmdFormat10BitRGBX}, + {"10 bit RGB-LE Video Range", bmdFormat10BitRGBXLE}}); + +static const std::vector hdr_metadata_value_names( + {"Red x", "Red y", "Green x", "Green y", "Blue x", "Blue y", "White Pt. x", "White Pt. y"}); + +const std::vector + light_level_controls_names({"D.M. Lum. Min", "D.M. Lum. Max", "CLL Max", "Frm Avg Light"}); + +const std::map eotf_modes({ + {"SDR", 0}, + {"HDR", 1}, + {"PQ", 2}, + {"HLG", 3}, + {"Auto PQ", -1}, +}); + +const std::map colourspaces( + {{"BT. 601", bmdColorspaceRec601}, + {"BT. 709", bmdColorspaceRec709}, + {"BT. 2020", bmdColorspaceRec2020}}); + +static const std::string shelf_button_qml(R"( +import QtQuick 2.12 +import BlackmagicSDI 1.0 +DecklinkShelfButton { +} +)"); +} // namespace + +BMDecklinkPlugin::BMDecklinkPlugin( + caf::actor_config &cfg, const utility::JsonStore &init_settings) + : ui::viewport::VideoOutputPlugin(cfg, init_settings, "BMDecklinkPlugin") { + + // Here we check that the Decklink drivers are installed. If not, we log a + // message and disable the plugin by throwing an exception in the constructor + // which will prevent the plugin from being created. This prevents the SDI + // UI component from appearing in xSTUDIO and confusing the user when they + // don't have a DeckLink device or drivers installed. + try { + + DecklinkOutput::check_decklink_installation(); + + } catch (const std::exception &e) { + + send_exit(this, caf::exit_reason::user_shutdown); + spdlog::info("Blackmagic Decklink SDI output disabled: {}", e.what()); + return; + } + + // add attributes used for configuring the SDI output + pixel_formats_ = add_string_choice_attribute( + "Pixel Format", "Pix Fmt", "10 bit YUV", utility::map_key_to_vec(bmd_pixel_formats)); + pixel_formats_->expose_in_ui_attrs_group("Decklink Settings"); + pixel_formats_->set_preference_path("/plugin/decklink/pixel_format"); + + sdi_output_is_running_ = add_boolean_attribute("Enabled", "Enabled", false); + sdi_output_is_running_->expose_in_ui_attrs_group("Decklink Settings"); + + status_message_ = add_string_attribute("Status", "Status", "Not Started"); + status_message_->expose_in_ui_attrs_group("Decklink Settings"); + + is_in_error_ = add_boolean_attribute("In Error", "In Error", false); + is_in_error_->expose_in_ui_attrs_group("Decklink Settings"); + + track_main_viewport_ = add_boolean_attribute("Track Viewport", "Track Viewport", true); + track_main_viewport_->expose_in_ui_attrs_group("Decklink Settings"); + + resolutions_ = add_string_choice_attribute("Output Resolution", "Out. Res."); + + resolutions_->expose_in_ui_attrs_group("Decklink Settings"); + resolutions_->set_preference_path("/plugin/decklink/output_resolution"); + + frame_rates_ = add_string_choice_attribute("Refresh Rate", "Hz"); + frame_rates_->expose_in_ui_attrs_group("Decklink Settings"); + frame_rates_->set_preference_path("/plugin/decklink/output_frame_rate"); + + start_stop_ = add_boolean_attribute("Start Stop", "Start Stop", false); + start_stop_->expose_in_ui_attrs_group("Decklink Settings"); + + auto_start_ = add_boolean_attribute("Auto Start", "Auto Start", false); + auto_start_->set_preference_path("/plugin/decklink/auto_start_sdi"); + + samples_water_level_ = + add_integer_attribute("Audio Samples Water Level", "Audio Samples Water Level", 4096); + samples_water_level_->set_preference_path("/plugin/decklink/audio_samps_water_level"); + + // This attr is currently not exposed to the user. It can be used to delay (or advance) the + // image that is sent to the SDI output vs. the main xSTUDIO viewport in order to exactly + // sync up the image that shows in xSTUDIO UI vs. the SDI display. This helps if you have a + // screening room with xSTUDIO UI on a monitor and the SDI output shown by a projector at + // the front of the room ... the person operating xSTUDIO may see the image on their screen + // is a few frames ahead of the SDI. However, for this to work we have to buffer a lot more + // frames in the playhead and this is expensive which is why I'm leaving it here but not + // exposing it. + video_pipeline_delay_milliseconds_ = + add_integer_attribute("Video Sync Delay", "Video Sync Delay", 0); + video_pipeline_delay_milliseconds_->set_preference_path( + "/plugin/decklink/video_sync_delay"); + video_pipeline_delay_milliseconds_->expose_in_ui_attrs_group("Decklink Settings"); + + audio_sync_delay_milliseconds_ = + add_integer_attribute("Audio Sync Delay", "Audio Sync Delay", 0); + audio_sync_delay_milliseconds_->set_preference_path( + "/plugin/decklink/new_audio_sync_delay"); + audio_sync_delay_milliseconds_->expose_in_ui_attrs_group("Decklink Settings"); + + disable_pc_audio_when_running_ = + add_boolean_attribute("Auto Disable PC Audio", "Auto Disable PC Audio", false); + disable_pc_audio_when_running_->set_preference_path( + "/plugin/decklink/disable_pc_audio_when_sdi_is_running"); + disable_pc_audio_when_running_->expose_in_ui_attrs_group("Decklink Settings"); + + make_hdr_attributes(); + + VideoOutputPlugin::finalise(); +} + +// This method is called when a new image buffer is ready to be displayed +void BMDecklinkPlugin::incoming_video_frame_callback(media_reader::ImageBufPtr incoming) { + if (dcl_output_ && dcl_output_->is_available()) { + dcl_output_->incoming_frame(incoming); + } +} + +void BMDecklinkPlugin::exit_cleanup() { + // the dcl_output_ has caf::actor handles pointing to this (BMDecklinkPlugin) + // 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) { + + if (status_data.contains("status_message") && status_data["status_message"].is_string()) { + status_message_->set_value(status_data["status_message"].get()); + } + if (status_data.contains("sdi_output_is_active") && + status_data["sdi_output_is_active"].is_boolean()) { + sdi_output_is_running_->set_value(status_data["sdi_output_is_active"].get()); + } + if (status_data.contains("error_state") && status_data["error_state"].is_boolean()) { + is_in_error_->set_value(status_data["error_state"].get()); + } +} + +void BMDecklinkPlugin::attribute_changed(const utility::Uuid &attribute_uuid, const int role) { + + if (dcl_output_) { + + if (resolutions_ && attribute_uuid == resolutions_->uuid() && + role == module::Attribute::Value) { + + const auto rates = dcl_output_->get_available_refresh_rates(resolutions_->value()); + frame_rates_->set_role_data(module::Attribute::StringChoices, rates); + + // pick a sensible refresh rate, if the current rate isn't available for new + // resolution + auto i = std::find( + rates.begin(), rates.end(), frame_rates_->value()); // prefer current rate + if (i == rates.end()) { + i = std::find(rates.begin(), rates.end(), "24.0"); // otherwise prefer 24.0 + if (i == rates.end()) { + i = std::find(rates.begin(), rates.end(), "60.0"); // use 60.0 otherwise + if (i == rates.end()) { + i = rates.begin(); + } + } + } + if (i != rates.end()) { + frame_rates_->set_value(*i); + } + + } else if (attribute_uuid == start_stop_->uuid()) { + + dcl_output_->StartStop(); + } + + if (attribute_uuid == pixel_formats_->uuid() || + attribute_uuid == resolutions_->uuid() || attribute_uuid == frame_rates_->uuid()) { + + try { + + if (bmd_pixel_formats.find(pixel_formats_->value()) == + bmd_pixel_formats.end()) { + throw std::runtime_error( + fmt::format("Invalid pixel format: {}", pixel_formats_->value())); + } + + const BMDPixelFormat pix_fmt = bmd_pixel_formats[pixel_formats_->value()]; + + + dcl_output_->set_display_mode( + resolutions_->value(), frame_rates_->value(), pix_fmt); + + } catch (std::exception &e) { + + status_message_->set_value(e.what()); + is_in_error_->set_value(true); + } + + } else if (attribute_uuid == track_main_viewport_->uuid()) { + + sync_geometry_to_main_viewport(track_main_viewport_->value()); + + } else if (attribute_uuid == samples_water_level_->uuid()) { + dcl_output_->set_audio_samples_water_level(samples_water_level_->value()); + } else if (attribute_uuid == audio_sync_delay_milliseconds_->uuid()) { + dcl_output_->set_audio_sync_delay_milliseconds( + audio_sync_delay_milliseconds_->value()); + } else if (attribute_uuid == video_pipeline_delay_milliseconds_->uuid()) { + video_delay_milliseconds(video_pipeline_delay_milliseconds_->value()); + } else if (attribute_uuid == disable_pc_audio_when_running_->uuid()) { + set_pc_audio_muting(); + } else if (attribute_uuid == sdi_output_is_running_->uuid()) { + set_pc_audio_muting(); + } else if (attribute_uuid == hdr_presets_->uuid()) { + + if (hdr_presets_data_.contains(hdr_presets_->value())) { + + const auto &vals = hdr_presets_data_[hdr_presets_->value()]; + if (vals.is_array() && vals.size() == 8) { + int idx = 0; + for (auto &setting_attr : hdr_metadata_settings_) { + setting_attr->set_value(vals[idx++].get()); + } + } + } + + } else if ( + role == module::Attribute::Value && + hdr_metadata_settings_uuids_.find(attribute_uuid) != + hdr_metadata_settings_uuids_.end()) { + set_hdr_mode_and_metadata(); + if (!prefs_save_scheduled_) { + prefs_save_scheduled_ = true; + delayed_anon_send( + caf::actor_cast(this), + std::chrono::seconds(5), + "save_hdr_colour_prefs"); + } + } + } + StandardPlugin::attribute_changed(attribute_uuid, role); +} + +audio::AudioOutputDevice * +BMDecklinkPlugin::make_audio_output_device(const utility::JsonStore &prefs) { + if (!dcl_output_ || !dcl_output_->is_available()) { + return nullptr; + } + return static_cast( + new DecklinkAudioOutputDevice(prefs, dcl_output_)); +} + +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()) { - status_message_->set_value("No DeckLink device detected."); - is_in_error_->set_value(true); - spdlog::warn("Decklink drivers found, but no DeckLink device is available."); + spdlog::warn("Decklink output unavailable."); return; } - - resolutions_->set_role_data( - module::Attribute::StringChoices, dcl_output_->output_resolution_names()); - - dcl_output_->set_audio_samples_water_level(samples_water_level_->value()); - dcl_output_->set_audio_sync_delay_milliseconds(audio_sync_delay_milliseconds_->value()); - - spdlog::info("Decklink Card Initialised"); - - // We register the UI here - register_viewport_dockable_widget( - "SDI Output Controls 2", - shelf_button_qml, // QML for a custom shelf button to activate the tool - "Show/Hide SDI Output Controls", // tooltip for the button, - 10.0f, // button position in the buttons bar - true, - "", // no dockable left/right widget - // qml code to create the top/bottom dockable widget - R"( - import QtQuick - import BlackmagicSDI 1.0 - DecklinkSettingsHorizontalWidget { - } - )"); - - // now we are set-up we can kick ourselves to fill in the refresh rate list etc. - attribute_changed(resolutions_->uuid(), module::Attribute::Value); - - if (auto_start_->value()) { - // start output immediately if auto_start_ is enabled (via prefs) - dcl_output_->StartStop(); - } - - sync_geometry_to_main_viewport(track_main_viewport_->value()); - - video_delay_milliseconds(video_pipeline_delay_milliseconds_->value()); - - // tell our viewport what sort of display we are. This info is used by - // the colour management system to try and pick an appropriate display - // transform - display_info("SDI Video Output", "Decklink", "Blackmagic Design", ""); - - // Here we add the Display attribute from the colour pipeline and the - // Fit mode attr from the offscreen viewport to an attribute group - // called "decklink viewport attrs" which is exposed in the UI. - // Our UI bar references this attribute group to make the dropdowns to - // control those two attributes. - anon_mail( - module::change_attribute_request_atom_v, - "Display", - int(module::Attribute::UIDataModels), - utility::JsonStore( - nlohmann::json::parse(R"(["decklink viewport attrs", "decklink ocio"])"))) - .send(colour_pipeline()); - - anon_mail( - module::change_attribute_request_atom_v, - "Fit (F)", - int(module::Attribute::UIDataModels), - utility::JsonStore(nlohmann::json::parse(R"(["decklink viewport attrs"])"))) - .send(offscreen_viewport()); - - } catch (std::exception &e) { - spdlog::critical("{} {}", __PRETTY_FUNCTION__, e.what()); - } -} - -void BMDecklinkPlugin::set_pc_audio_muting() { - - // we can get access to the - auto pc_audio_output_actor = - system().registry().template get(pc_audio_output_registry); - - if (pc_audio_output_actor) { - const bool mute = - disable_pc_audio_when_running_->value() && sdi_output_is_running_->value(); - anon_send( - pc_audio_output_actor, audio::set_override_volume_atom_v, mute ? 0.0f : -1.0f); - } -} - -void BMDecklinkPlugin::make_hdr_attributes() { - - // Add the HDR mode attribute - hdr_mode_ = add_string_choice_attribute( - "HDR Mode", "HDR Mode", "SDR", utility::map_key_to_vec(eotf_modes)); - hdr_mode_->expose_in_ui_attrs_group("Decklink HDR Settings"); - hdr_mode_->set_preference_path("/plugin/decklink/hdr_mode"); - hdr_metadata_settings_uuids_.insert(hdr_mode_->uuid()); - - // Add the Colourspace mode attribute - colourspace_ = add_string_choice_attribute( - "Colour Space", "Colour Space", "BT. 709", utility::map_key_to_vec(colourspaces)); - colourspace_->expose_in_ui_attrs_group("Decklink HDR Settings"); - colourspace_->set_preference_path("/plugin/decklink/colourspace"); - hdr_metadata_settings_uuids_.insert(colourspace_->uuid()); - - // Fetch the ocio display name match string for auto HDR mode switching - auto prefs = global_store::GlobalStoreHelper(system()); - try { - ocio_display_hdr_match_string_ = utility::to_lower( - prefs.value("/plugin/decklink/ocio_display_hdr_match_string")); - } catch (std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - - std::array colour_settings({0.64, 0.33, 0.30, 0.6, 0.15, 0.06, 0.3127, 0.329}); - try { - auto colour_vals = - prefs.value("/plugin/decklink/hdr_colour_metadata_values"); - if (colour_vals.is_array() && colour_vals.size() == 8) { - int idx = 0; - for (auto &o : colour_vals) { - if (!o.is_number()) { - throw std::runtime_error( - "hdr_colour_metadata_values should be 8 float values."); - } - colour_settings[idx++] = o.get(); - } - } else { - throw std::runtime_error( - "hdr_colour_metadata_values should be an array of 8 float values."); - } - } catch (std::exception &e) { - spdlog::warn( - "{}: Failed to load perference /plugin/decklink/hdr_colour_metadata_values: {}", - __PRETTY_FUNCTION__, - e.what()); - } - - int idx = 0; - for (const auto &o : hdr_metadata_value_names) { - hdr_metadata_settings_.push_back(add_float_attribute(o, o, colour_settings[idx++])); - hdr_metadata_settings_.back()->expose_in_ui_attrs_group("Decklink HDR Values"); - hdr_metadata_settings_uuids_.insert(hdr_metadata_settings_.back()->uuid()); - } - - - std::array lum_settings({0.0005, 1000.0, 1000.0, 400.0}); - try { - auto lum__vals = - prefs.value("/plugin/decklink/hdr_luminance_metadata_values"); - if (lum__vals.is_array() && lum__vals.size() == 4) { - int idx = 0; - for (auto &o : lum__vals) { - if (!o.is_number()) { - throw std::runtime_error( - "hdr_luminance_metadata_values should be 4 float values."); - } - lum_settings[idx++] = o.get(); - } - } else { - throw std::runtime_error( - "hdr_luminance_metadata_values should be an array of 4 float values."); - } - } catch (std::exception &e) { - spdlog::warn( - "{}: Failed to load perference /plugin/decklink/hdr_luminance_metadata_values: {}", - __PRETTY_FUNCTION__, - e.what()); - } - - idx = 0; - for (const auto &o : light_level_controls_names) { - hdr_metadata_lightlevel_.push_back(add_float_attribute(o, o, lum_settings[idx++])); - hdr_metadata_lightlevel_.back()->expose_in_ui_attrs_group("Decklink HDR Values"); - hdr_metadata_settings_uuids_.insert(hdr_metadata_lightlevel_.back()->uuid()); - } - - try { - - hdr_presets_ = add_string_choice_attribute("HDR Presets", "HDR Presets", "", {}); - hdr_presets_->expose_in_ui_attrs_group("Decklink HDR Settings"); - - hdr_presets_data_ = prefs.value("/plugin/decklink/hdr_presets"); - std::vector presets_names; - - if (hdr_presets_data_.is_object()) { - for (auto it = hdr_presets_data_.begin(); it != hdr_presets_data_.end(); it++) { - presets_names.push_back(it.key()); - } - hdr_presets_->set_role_data(module::Attribute::StringChoices, presets_names); - } else { - throw std::runtime_error("hdr_presets should a json dictionary."); - } - - } catch (std::exception &e) { - spdlog::warn( - "{}: Failed to load perference /plugin/decklink/hdr_presets: {}", - __PRETTY_FUNCTION__, - e.what()); - } -} - -void BMDecklinkPlugin::set_hdr_mode_and_metadata() { - - if (!dcl_output_) - return; - - HDRMetadata metadata; - auto p = eotf_modes.find(hdr_mode_->value()); - metadata.EOTF = p != eotf_modes.end() ? p->second : 0; - - if (metadata.EOTF == -1) { - // auto turn on HDR mode if the ocio display includes the string - // 'ocio_display_hdr_match_string_' which was set from our preferences. So for example, - // if ocio_display_hdr_match_string_ is 'hdr' then if the ocio display is 'Screening - // Room (HDR)' then HDR output is turned on - if (utility::to_lower(get_ocio_display_name()).find(ocio_display_hdr_match_string_) != - std::string::npos) { - metadata.EOTF = 2; // PQ (Perceptual Quantization or something) - } else { - metadata.EOTF = 0; - } - } - - auto q = colourspaces.find(colourspace_->value()); - metadata.colourspace_ = q != colourspaces.end() ? q->second : bmdColorspaceRec709; - - for (size_t i = 0; i < hdr_metadata_settings_.size(); ++i) { - metadata.referencePrimaries[i] = hdr_metadata_settings_[i]->value(); - } - - for (size_t i = 0; i < hdr_metadata_lightlevel_.size(); ++i) { - metadata.luminanceSettings[i] = hdr_metadata_lightlevel_[i]->value(); - } - - dcl_output_->set_hdr_metadata(metadata); -} - -void BMDecklinkPlugin::save_hdr_colour_prefs() { - - utility::JsonStore cvals(nlohmann::json::parse("[]")); - utility::JsonStore lvals(nlohmann::json::parse("[]")); - for (size_t i = 0; i < hdr_metadata_settings_.size(); ++i) { - cvals.push_back(hdr_metadata_settings_[i]->value()); - } - - for (size_t i = 0; i < hdr_metadata_lightlevel_.size(); ++i) { - lvals.push_back(hdr_metadata_lightlevel_[i]->value()); - } - - // Fetch the ocio display name match string for auto HDR mode switching - auto prefs = global_store::GlobalStoreHelper(system()); - try { - prefs.set_value(cvals, "/plugin/decklink/hdr_colour_metadata_values"); - prefs.set_value(lvals, "/plugin/decklink/hdr_luminance_metadata_values"); - } catch (std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - prefs_save_scheduled_ = false; -} - -std::string BMDecklinkPlugin::get_ocio_display_name() { - - if (!colour_pipeline()) - return std::string(); - - try { - - scoped_actor sys{system()}; - - auto ocio_display = utility::request_receive( - *sys, colour_pipeline(), module::attribute_value_atom_v, std::string("Display")); - - if (ocio_display.is_string()) { - return ocio_display.get(); - } - - } catch (const std::exception &e) { - spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); - } - - return std::string(); -} - -BMDecklinkPlugin::~BMDecklinkPlugin() {} - -XSTUDIO_PLUGIN_DECLARE_BEGIN() - -XSTUDIO_REGISTER_PLUGIN( - BMDecklinkPlugin, - BMDecklinkPlugin::PLUGIN_UUID, - BlackMagic Decklink Viewport, - plugin_manager::PluginFlags::PF_VIDEO_OUTPUT, - false, - Ted Waine, - BlackMagic Decklink SDI Output Plugin, - 1.0.0) - -XSTUDIO_PLUGIN_DECLARE_END() + + set_hdr_mode_and_metadata(); + + if (!dcl_output_->is_available()) { + status_message_->set_value("No DeckLink device detected."); + is_in_error_->set_value(true); + spdlog::warn("Decklink drivers found, but no DeckLink device is available."); + return; + } + + resolutions_->set_role_data( + module::Attribute::StringChoices, dcl_output_->output_resolution_names()); + + dcl_output_->set_audio_samples_water_level(samples_water_level_->value()); + dcl_output_->set_audio_sync_delay_milliseconds(audio_sync_delay_milliseconds_->value()); + + spdlog::info("Decklink Card Initialised"); + + // We register the UI here + register_viewport_dockable_widget( + "SDI Output Controls 2", + shelf_button_qml, // QML for a custom shelf button to activate the tool + "Show/Hide SDI Output Controls", // tooltip for the button, + 10.0f, // button position in the buttons bar + true, + "", // no dockable left/right widget + // qml code to create the top/bottom dockable widget + R"( + import QtQuick + import BlackmagicSDI 1.0 + DecklinkSettingsHorizontalWidget { + } + )"); + + // now we are set-up we can kick ourselves to fill in the refresh rate list etc. + attribute_changed(resolutions_->uuid(), module::Attribute::Value); + + if (auto_start_->value()) { + // start output immediately if auto_start_ is enabled (via prefs) + dcl_output_->StartStop(); + } + + sync_geometry_to_main_viewport(track_main_viewport_->value()); + + video_delay_milliseconds(video_pipeline_delay_milliseconds_->value()); + + // tell our viewport what sort of display we are. This info is used by + // the colour management system to try and pick an appropriate display + // transform + display_info("SDI Video Output", "Decklink", "Blackmagic Design", ""); + + // Here we add the Display attribute from the colour pipeline and the + // Fit mode attr from the offscreen viewport to an attribute group + // called "decklink viewport attrs" which is exposed in the UI. + // Our UI bar references this attribute group to make the dropdowns to + // control those two attributes. + anon_mail( + module::change_attribute_request_atom_v, + "Display", + int(module::Attribute::UIDataModels), + utility::JsonStore( + nlohmann::json::parse(R"(["decklink viewport attrs", "decklink ocio"])"))) + .send(colour_pipeline()); + + anon_mail( + module::change_attribute_request_atom_v, + "Fit (F)", + int(module::Attribute::UIDataModels), + utility::JsonStore(nlohmann::json::parse(R"(["decklink viewport attrs"])"))) + .send(offscreen_viewport()); + + } catch (std::exception &e) { + spdlog::critical("{} {}", __PRETTY_FUNCTION__, e.what()); + } +} + +void BMDecklinkPlugin::set_pc_audio_muting() { + + // we can get access to the + auto pc_audio_output_actor = + system().registry().template get(pc_audio_output_registry); + + if (pc_audio_output_actor) { + const bool mute = + disable_pc_audio_when_running_->value() && sdi_output_is_running_->value(); + anon_send( + pc_audio_output_actor, audio::set_override_volume_atom_v, mute ? 0.0f : -1.0f); + } +} + +void BMDecklinkPlugin::make_hdr_attributes() { + + // Add the HDR mode attribute + hdr_mode_ = add_string_choice_attribute( + "HDR Mode", "HDR Mode", "SDR", utility::map_key_to_vec(eotf_modes)); + hdr_mode_->expose_in_ui_attrs_group("Decklink HDR Settings"); + hdr_mode_->set_preference_path("/plugin/decklink/hdr_mode"); + hdr_metadata_settings_uuids_.insert(hdr_mode_->uuid()); + + // Add the Colourspace mode attribute + colourspace_ = add_string_choice_attribute( + "Colour Space", "Colour Space", "BT. 709", utility::map_key_to_vec(colourspaces)); + colourspace_->expose_in_ui_attrs_group("Decklink HDR Settings"); + colourspace_->set_preference_path("/plugin/decklink/colourspace"); + hdr_metadata_settings_uuids_.insert(colourspace_->uuid()); + + // Fetch the ocio display name match string for auto HDR mode switching + auto prefs = global_store::GlobalStoreHelper(system()); + try { + ocio_display_hdr_match_string_ = utility::to_lower( + prefs.value("/plugin/decklink/ocio_display_hdr_match_string")); + } catch (std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } + + std::array colour_settings({0.64, 0.33, 0.30, 0.6, 0.15, 0.06, 0.3127, 0.329}); + try { + auto colour_vals = + prefs.value("/plugin/decklink/hdr_colour_metadata_values"); + if (colour_vals.is_array() && colour_vals.size() == 8) { + int idx = 0; + for (auto &o : colour_vals) { + if (!o.is_number()) { + throw std::runtime_error( + "hdr_colour_metadata_values should be 8 float values."); + } + colour_settings[idx++] = o.get(); + } + } else { + throw std::runtime_error( + "hdr_colour_metadata_values should be an array of 8 float values."); + } + } catch (std::exception &e) { + spdlog::warn( + "{}: Failed to load perference /plugin/decklink/hdr_colour_metadata_values: {}", + __PRETTY_FUNCTION__, + e.what()); + } + + int idx = 0; + for (const auto &o : hdr_metadata_value_names) { + hdr_metadata_settings_.push_back(add_float_attribute(o, o, colour_settings[idx++])); + hdr_metadata_settings_.back()->expose_in_ui_attrs_group("Decklink HDR Values"); + hdr_metadata_settings_uuids_.insert(hdr_metadata_settings_.back()->uuid()); + } + + + std::array lum_settings({0.0005, 1000.0, 1000.0, 400.0}); + try { + auto lum__vals = + prefs.value("/plugin/decklink/hdr_luminance_metadata_values"); + if (lum__vals.is_array() && lum__vals.size() == 4) { + int idx = 0; + for (auto &o : lum__vals) { + if (!o.is_number()) { + throw std::runtime_error( + "hdr_luminance_metadata_values should be 4 float values."); + } + lum_settings[idx++] = o.get(); + } + } else { + throw std::runtime_error( + "hdr_luminance_metadata_values should be an array of 4 float values."); + } + } catch (std::exception &e) { + spdlog::warn( + "{}: Failed to load perference /plugin/decklink/hdr_luminance_metadata_values: {}", + __PRETTY_FUNCTION__, + e.what()); + } + + idx = 0; + for (const auto &o : light_level_controls_names) { + hdr_metadata_lightlevel_.push_back(add_float_attribute(o, o, lum_settings[idx++])); + hdr_metadata_lightlevel_.back()->expose_in_ui_attrs_group("Decklink HDR Values"); + hdr_metadata_settings_uuids_.insert(hdr_metadata_lightlevel_.back()->uuid()); + } + + try { + + hdr_presets_ = add_string_choice_attribute("HDR Presets", "HDR Presets", "", {}); + hdr_presets_->expose_in_ui_attrs_group("Decklink HDR Settings"); + + hdr_presets_data_ = prefs.value("/plugin/decklink/hdr_presets"); + std::vector presets_names; + + if (hdr_presets_data_.is_object()) { + for (auto it = hdr_presets_data_.begin(); it != hdr_presets_data_.end(); it++) { + presets_names.push_back(it.key()); + } + hdr_presets_->set_role_data(module::Attribute::StringChoices, presets_names); + } else { + throw std::runtime_error("hdr_presets should a json dictionary."); + } + + } catch (std::exception &e) { + spdlog::warn( + "{}: Failed to load perference /plugin/decklink/hdr_presets: {}", + __PRETTY_FUNCTION__, + e.what()); + } +} + +void BMDecklinkPlugin::set_hdr_mode_and_metadata() { + + if (!dcl_output_) + return; + + HDRMetadata metadata; + auto p = eotf_modes.find(hdr_mode_->value()); + metadata.EOTF = p != eotf_modes.end() ? p->second : 0; + + if (metadata.EOTF == -1) { + // auto turn on HDR mode if the ocio display includes the string + // 'ocio_display_hdr_match_string_' which was set from our preferences. So for example, + // if ocio_display_hdr_match_string_ is 'hdr' then if the ocio display is 'Screening + // Room (HDR)' then HDR output is turned on + if (utility::to_lower(get_ocio_display_name()).find(ocio_display_hdr_match_string_) != + std::string::npos) { + metadata.EOTF = 2; // PQ (Perceptual Quantization or something) + } else { + metadata.EOTF = 0; + } + } + + auto q = colourspaces.find(colourspace_->value()); + metadata.colourspace_ = q != colourspaces.end() ? q->second : bmdColorspaceRec709; + + for (size_t i = 0; i < hdr_metadata_settings_.size(); ++i) { + metadata.referencePrimaries[i] = hdr_metadata_settings_[i]->value(); + } + + for (size_t i = 0; i < hdr_metadata_lightlevel_.size(); ++i) { + metadata.luminanceSettings[i] = hdr_metadata_lightlevel_[i]->value(); + } + + dcl_output_->set_hdr_metadata(metadata); +} + +void BMDecklinkPlugin::save_hdr_colour_prefs() { + + utility::JsonStore cvals(nlohmann::json::parse("[]")); + utility::JsonStore lvals(nlohmann::json::parse("[]")); + for (size_t i = 0; i < hdr_metadata_settings_.size(); ++i) { + cvals.push_back(hdr_metadata_settings_[i]->value()); + } + + for (size_t i = 0; i < hdr_metadata_lightlevel_.size(); ++i) { + lvals.push_back(hdr_metadata_lightlevel_[i]->value()); + } + + // Fetch the ocio display name match string for auto HDR mode switching + auto prefs = global_store::GlobalStoreHelper(system()); + try { + prefs.set_value(cvals, "/plugin/decklink/hdr_colour_metadata_values"); + prefs.set_value(lvals, "/plugin/decklink/hdr_luminance_metadata_values"); + } catch (std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } + prefs_save_scheduled_ = false; +} + +std::string BMDecklinkPlugin::get_ocio_display_name() { + + if (!colour_pipeline()) + return std::string(); + + try { + + scoped_actor sys{system()}; + + auto ocio_display = utility::request_receive( + *sys, colour_pipeline(), module::attribute_value_atom_v, std::string("Display")); + + if (ocio_display.is_string()) { + return ocio_display.get(); + } + + } catch (const std::exception &e) { + spdlog::warn("{} {}", __PRETTY_FUNCTION__, e.what()); + } + + return std::string(); +} + +BMDecklinkPlugin::~BMDecklinkPlugin() {} + +XSTUDIO_PLUGIN_DECLARE_BEGIN() + +XSTUDIO_REGISTER_PLUGIN( + BMDecklinkPlugin, + BMDecklinkPlugin::PLUGIN_UUID, + BlackMagic Decklink Viewport, + plugin_manager::PluginFlags::PF_VIDEO_OUTPUT, + false, + Ted Waine, + BlackMagic Decklink SDI Output Plugin, + 1.0.0) + +XSTUDIO_PLUGIN_DECLARE_END() 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 5f8cc8b6a..7a6fd8beb 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 @@ -1,103 +1,103 @@ -/* -LICENSE-START- -** Copyright (c) 2009 Blackmagic Design -** -** Permission is hereby granted, free of charge, to any person or organization -** obtaining a copy of the software and accompanying documentation (the -** "Software") to use, reproduce, display, distribute, sub-license, execute, -** and transmit the Software, and to prepare derivative works of the Software, -** and to permit third-parties to whom the Software is furnished to do so, in -** accordance with: -** -** (1) if the Software is obtained from Blackmagic Design, the End User License -** Agreement for the Software Development Kit ("EULA") available at -** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or -** -** (2) if the Software is obtained from any third party, such licensing terms -** as notified by that third party, -** -** and all subject to the following: -** -** (3) the copyright notices in the Software and this entire statement, -** including the above license grant, this restriction and the following -** disclaimer, must be included in all copies of the Software, in whole or in -** part, and all derivative works of the Software, unless such copies or -** derivative works are solely in the form of machine-executable object code -** generated by a source language processor. -** -** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT -** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE -** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, -** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -** DEALINGS IN THE SOFTWARE. -** -** A copy of the Software is available free of charge at -** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA. -** -** -LICENSE-END- -**/ - -#include -#include -#include -#include - -#include "DeckLinkAPI.h" - -#define kDeckLinkAPI_Name "libDeckLinkAPI.so" -#define KDeckLinkPreviewAPI_Name "libDeckLinkPreviewAPI.so" - -typedef IDeckLinkIterator *(*CreateIteratorFunc)(void); -typedef IDeckLinkAPIInformation *(*CreateAPIInformationFunc)(void); -typedef IDeckLinkGLScreenPreviewHelper *(*CreateOpenGLScreenPreviewHelperFunc)(void); -typedef IDeckLinkGLScreenPreviewHelper *(*CreateOpenGL3ScreenPreviewHelperFunc)(void); -typedef IDeckLinkVideoConversion *(*CreateVideoConversionInstanceFunc)(void); -typedef IDeckLinkDiscovery *(*CreateDeckLinkDiscoveryInstanceFunc)(void); -typedef IDeckLinkVideoFrameAncillaryPackets *(*CreateVideoFrameAncillaryPacketsInstanceFunc)( - void); - -static pthread_once_t gDeckLinkOnceControl = PTHREAD_ONCE_INIT; -static pthread_once_t gPreviewOnceControl = PTHREAD_ONCE_INIT; - -static bool gLoadedDeckLinkAPI = false; - -static CreateIteratorFunc gCreateIteratorFunc = NULL; -static CreateAPIInformationFunc gCreateAPIInformationFunc = NULL; -static CreateOpenGLScreenPreviewHelperFunc gCreateOpenGLPreviewFunc = NULL; -static CreateOpenGL3ScreenPreviewHelperFunc gCreateOpenGL3PreviewFunc = NULL; -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); +/* -LICENSE-START- +** Copyright (c) 2009 Blackmagic Design +** +** Permission is hereby granted, free of charge, to any person or organization +** obtaining a copy of the software and accompanying documentation (the +** "Software") to use, reproduce, display, distribute, sub-license, execute, +** and transmit the Software, and to prepare derivative works of the Software, +** and to permit third-parties to whom the Software is furnished to do so, in +** accordance with: +** +** (1) if the Software is obtained from Blackmagic Design, the End User License +** Agreement for the Software Development Kit ("EULA") available at +** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or +** +** (2) if the Software is obtained from any third party, such licensing terms +** as notified by that third party, +** +** and all subject to the following: +** +** (3) the copyright notices in the Software and this entire statement, +** including the above license grant, this restriction and the following +** disclaimer, must be included in all copies of the Software, in whole or in +** part, and all derivative works of the Software, unless such copies or +** derivative works are solely in the form of machine-executable object code +** generated by a source language processor. +** +** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +** DEALINGS IN THE SOFTWARE. +** +** A copy of the Software is available free of charge at +** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA. +** +** -LICENSE-END- +**/ + +#include +#include +#include +#include + +#include "DeckLinkAPI.h" + +#define kDeckLinkAPI_Name "libDeckLinkAPI.so" +#define KDeckLinkPreviewAPI_Name "libDeckLinkPreviewAPI.so" + +typedef IDeckLinkIterator *(*CreateIteratorFunc)(void); +typedef IDeckLinkAPIInformation *(*CreateAPIInformationFunc)(void); +typedef IDeckLinkGLScreenPreviewHelper *(*CreateOpenGLScreenPreviewHelperFunc)(void); +typedef IDeckLinkGLScreenPreviewHelper *(*CreateOpenGL3ScreenPreviewHelperFunc)(void); +typedef IDeckLinkVideoConversion *(*CreateVideoConversionInstanceFunc)(void); +typedef IDeckLinkDiscovery *(*CreateDeckLinkDiscoveryInstanceFunc)(void); +typedef IDeckLinkVideoFrameAncillaryPackets *(*CreateVideoFrameAncillaryPacketsInstanceFunc)( + void); + +static pthread_once_t gDeckLinkOnceControl = PTHREAD_ONCE_INIT; +static pthread_once_t gPreviewOnceControl = PTHREAD_ONCE_INIT; + +static bool gLoadedDeckLinkAPI = false; + +static CreateIteratorFunc gCreateIteratorFunc = NULL; +static CreateAPIInformationFunc gCreateAPIInformationFunc = NULL; +static CreateOpenGLScreenPreviewHelperFunc gCreateOpenGLPreviewFunc = NULL; +static CreateOpenGL3ScreenPreviewHelperFunc gCreateOpenGL3PreviewFunc = NULL; +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; @@ -105,148 +105,142 @@ static void *GetSymbolAddress( 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; - - libraryHandle = dlopen(kDeckLinkAPI_Name, RTLD_NOW | RTLD_GLOBAL); - if (!libraryHandle) { - fprintf(stderr, "%s\n", dlerror()); - return; - } - - gLoadedDeckLinkAPI = true; - - gCreateIteratorFunc = - (CreateIteratorFunc)dlsym(libraryHandle, "CreateDeckLinkIteratorInstance_0004"); - if (!gCreateIteratorFunc) - fprintf(stderr, "%s\n", dlerror()); - gCreateAPIInformationFunc = (CreateAPIInformationFunc)dlsym( - libraryHandle, "CreateDeckLinkAPIInformationInstance_0001"); - if (!gCreateAPIInformationFunc) - 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)GetSymbolAddress( - libraryHandle, - "CreateVideoFrameAncillaryPacketsInstance_0002", - "CreateVideoFrameAncillaryPacketsInstance_0001", - "ancillary packets"); -} - -static void InitDeckLinkPreviewAPI(void) { - void *libraryHandle; - - libraryHandle = dlopen(KDeckLinkPreviewAPI_Name, RTLD_NOW | RTLD_GLOBAL); - if (!libraryHandle) { - fprintf(stderr, "%s\n", dlerror()); - return; - } - gCreateOpenGLPreviewFunc = (CreateOpenGLScreenPreviewHelperFunc)dlsym( - libraryHandle, "CreateOpenGLScreenPreviewHelper_0002"); - if (!gCreateOpenGLPreviewFunc) - fprintf(stderr, "%s\n", dlerror()); - gCreateOpenGL3PreviewFunc = (CreateOpenGL3ScreenPreviewHelperFunc)dlsym( - libraryHandle, "CreateOpenGL3ScreenPreviewHelper_0002"); - if (!gCreateOpenGL3PreviewFunc) - fprintf(stderr, "%s\n", dlerror()); -} - -bool IsDeckLinkAPIPresent(void) { - // If the DeckLink API dynamic library was successfully loaded, return this knowledge to the - // caller - return gLoadedDeckLinkAPI; -} - -extern "C" const char *GetDeckLinkVideoConversionSymbolName(void) { - pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI); - return gVideoConversionSymbolName; -} - -extern "C" const char *GetDeckLinkAncillaryPacketsSymbolName(void) { - pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI); - return gAncillaryPacketsSymbolName; -} - -IDeckLinkIterator *CreateDeckLinkIteratorInstance(void) { - pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI); - - if (gCreateIteratorFunc == NULL) - return NULL; - return gCreateIteratorFunc(); -} - -IDeckLinkAPIInformation *CreateDeckLinkAPIInformationInstance(void) { - pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI); - - if (gCreateAPIInformationFunc == NULL) - return NULL; - return gCreateAPIInformationFunc(); -} - -IDeckLinkGLScreenPreviewHelper *CreateOpenGLScreenPreviewHelper(void) { - pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI); - pthread_once(&gPreviewOnceControl, InitDeckLinkPreviewAPI); - - if (gCreateOpenGLPreviewFunc == NULL) - return NULL; - return gCreateOpenGLPreviewFunc(); -} - -IDeckLinkGLScreenPreviewHelper *CreateOpenGL3ScreenPreviewHelper(void) { - pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI); - pthread_once(&gPreviewOnceControl, InitDeckLinkPreviewAPI); - - if (gCreateOpenGL3PreviewFunc == NULL) - return NULL; - return gCreateOpenGL3PreviewFunc(); -} - -IDeckLinkVideoConversion *CreateVideoConversionInstance(void) { - pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI); - - if (gCreateVideoConversionFunc == NULL) - return NULL; - return gCreateVideoConversionFunc(); -} - -IDeckLinkDiscovery *CreateDeckLinkDiscoveryInstance(void) { - pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI); - - if (gCreateDeckLinkDiscoveryFunc == NULL) - return NULL; - return gCreateDeckLinkDiscoveryFunc(); -} - -IDeckLinkVideoFrameAncillaryPackets *CreateVideoFrameAncillaryPacketsInstance(void) { - pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI); - - if (gCreateVideoFrameAncillaryPacketsFunc == NULL) - return NULL; - return gCreateVideoFrameAncillaryPacketsFunc(); -} + + fprintf( + stderr, + "DeckLink API missing %s symbols %s and %s\n", + symbolDescription, + primarySymbol, + fallbackSymbol); + return NULL; +} + +static void InitDeckLinkAPI(void) { + void *libraryHandle; + + libraryHandle = dlopen(kDeckLinkAPI_Name, RTLD_NOW | RTLD_GLOBAL); + if (!libraryHandle) { + fprintf(stderr, "%s\n", dlerror()); + return; + } + + gLoadedDeckLinkAPI = true; + + gCreateIteratorFunc = + (CreateIteratorFunc)dlsym(libraryHandle, "CreateDeckLinkIteratorInstance_0004"); + if (!gCreateIteratorFunc) + fprintf(stderr, "%s\n", dlerror()); + gCreateAPIInformationFunc = (CreateAPIInformationFunc)dlsym( + libraryHandle, "CreateDeckLinkAPIInformationInstance_0001"); + if (!gCreateAPIInformationFunc) + 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)GetSymbolAddress( + libraryHandle, + "CreateVideoFrameAncillaryPacketsInstance_0002", + "CreateVideoFrameAncillaryPacketsInstance_0001", + "ancillary packets"); +} + +static void InitDeckLinkPreviewAPI(void) { + void *libraryHandle; + + libraryHandle = dlopen(KDeckLinkPreviewAPI_Name, RTLD_NOW | RTLD_GLOBAL); + if (!libraryHandle) { + fprintf(stderr, "%s\n", dlerror()); + return; + } + gCreateOpenGLPreviewFunc = (CreateOpenGLScreenPreviewHelperFunc)dlsym( + libraryHandle, "CreateOpenGLScreenPreviewHelper_0002"); + if (!gCreateOpenGLPreviewFunc) + fprintf(stderr, "%s\n", dlerror()); + gCreateOpenGL3PreviewFunc = (CreateOpenGL3ScreenPreviewHelperFunc)dlsym( + libraryHandle, "CreateOpenGL3ScreenPreviewHelper_0002"); + if (!gCreateOpenGL3PreviewFunc) + fprintf(stderr, "%s\n", dlerror()); +} + +bool IsDeckLinkAPIPresent(void) { + // If the DeckLink API dynamic library was successfully loaded, return this knowledge to the + // caller + return gLoadedDeckLinkAPI; +} + +extern "C" const char *GetDeckLinkVideoConversionSymbolName(void) { + pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI); + return gVideoConversionSymbolName; +} + +extern "C" const char *GetDeckLinkAncillaryPacketsSymbolName(void) { + pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI); + return gAncillaryPacketsSymbolName; +} + +IDeckLinkIterator *CreateDeckLinkIteratorInstance(void) { + pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI); + + if (gCreateIteratorFunc == NULL) + return NULL; + return gCreateIteratorFunc(); +} + +IDeckLinkAPIInformation *CreateDeckLinkAPIInformationInstance(void) { + pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI); + + if (gCreateAPIInformationFunc == NULL) + return NULL; + return gCreateAPIInformationFunc(); +} + +IDeckLinkGLScreenPreviewHelper *CreateOpenGLScreenPreviewHelper(void) { + pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI); + pthread_once(&gPreviewOnceControl, InitDeckLinkPreviewAPI); + + if (gCreateOpenGLPreviewFunc == NULL) + return NULL; + return gCreateOpenGLPreviewFunc(); +} + +IDeckLinkGLScreenPreviewHelper *CreateOpenGL3ScreenPreviewHelper(void) { + pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI); + pthread_once(&gPreviewOnceControl, InitDeckLinkPreviewAPI); + + if (gCreateOpenGL3PreviewFunc == NULL) + return NULL; + return gCreateOpenGL3PreviewFunc(); +} + +IDeckLinkVideoConversion *CreateVideoConversionInstance(void) { + pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI); + + if (gCreateVideoConversionFunc == NULL) + return NULL; + return gCreateVideoConversionFunc(); +} + +IDeckLinkDiscovery *CreateDeckLinkDiscoveryInstance(void) { + pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI); + + if (gCreateDeckLinkDiscoveryFunc == NULL) + return NULL; + return gCreateDeckLinkDiscoveryFunc(); +} + +IDeckLinkVideoFrameAncillaryPackets *CreateVideoFrameAncillaryPacketsInstance(void) { + pthread_once(&gDeckLinkOnceControl, InitDeckLinkAPI); + + if (gCreateVideoFrameAncillaryPacketsFunc == NULL) + return NULL; + return gCreateVideoFrameAncillaryPacketsFunc(); +}